Improved edge-to-edge implementation on all screens

Large number of code formatting improvements
This commit is contained in:
Juha Heinanen
2026-06-18 08:33:12 +03:00
parent 074eaecb4f
commit 66e6656161
14 changed files with 549 additions and 971 deletions
@@ -2,13 +2,13 @@ package com.tutpro.baresip
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -56,17 +56,11 @@ fun AboutScreen(onBack: () -> Unit) {
} }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
@@ -8,14 +8,15 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -137,9 +138,8 @@ private fun AccountScreen(
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME)
resumeToggle = System.currentTimeMillis() resumeToggle = System.currentTimeMillis()
}
} }
lifecycleOwner.lifecycle.addObserver(observer) lifecycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose {
@@ -164,23 +164,14 @@ private fun AccountScreen(
} }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(text = acc.text(), fontWeight = FontWeight.Bold)
text = acc.text(),
fontWeight = FontWeight.Bold
)
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -199,10 +190,7 @@ private fun AccountScreen(
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
actions = { actions = {
IconButton(onClick = checkOnClick) { IconButton(onClick = checkOnClick) {
Icon( Icon(imageVector = Icons.Filled.Check, contentDescription = "Check")
imageVector = Icons.Filled.Check,
contentDescription = "Check"
)
} }
}, },
) )
@@ -253,10 +241,7 @@ private fun AccountContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(fontSize = 18.sp), textStyle = TextStyle(fontSize = 18.sp),
label = { label = {
Text( Text(text = label, fontWeight = FontWeight.Bold)
text = label,
fontWeight = FontWeight.Bold
)
}, },
colors = OutlinedTextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@@ -271,9 +256,7 @@ private fun AccountContent(
@Composable @Composable
fun AoR(toggle: Long) { fun AoR(toggle: Long) {
Column( Column(
Modifier Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp),
.fillMaxWidth()
.padding(top = 8.dp, end = 10.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (ua.account.isMobile && android.os.Build.VERSION.SDK_INT >= 29) { if (ua.account.isMobile && android.os.Build.VERSION.SDK_INT >= 29) {
@@ -294,7 +277,8 @@ private fun AccountContent(
value = if (smsNumber != null) "tel:$smsNumber" else stringResource(R.string.not_available), value = if (smsNumber != null) "tel:$smsNumber" else stringResource(R.string.not_available),
label = stringResource(R.string.tel_uri_messages) label = stringResource(R.string.tel_uri_messages)
) )
} else { }
else {
val currentSubId = remember(toggle) { val currentSubId = remember(toggle) {
if (voiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) if (voiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID)
voiceSubId voiceSubId
@@ -312,12 +296,12 @@ private fun AccountContent(
label = stringResource(R.string.tel_uri) label = stringResource(R.string.tel_uri)
) )
} }
} else { }
else
AoRField( AoRField(
value = if (ua.account.isMobile) stringResource(R.string.not_available) else ua.account.luri, value = if (ua.account.isMobile) stringResource(R.string.not_available) else ua.account.luri,
label = stringResource(if (ua.account.isMobile) R.string.tel_uri else R.string.sip_uri) label = stringResource(if (ua.account.isMobile) R.string.tel_uri else R.string.sip_uri)
) )
}
} }
} }
@@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
@@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -69,23 +69,14 @@ fun NavGraphBuilder.accountsScreenRoute(navController: NavController) {
@Composable @Composable
fun AccountsScreen(navController: NavController) { fun AccountsScreen(navController: NavController) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(text = stringResource(R.string.accounts), fontWeight = FontWeight.Bold)
text = stringResource(R.string.accounts),
fontWeight = FontWeight.Bold
)
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -17,13 +17,13 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -101,23 +101,14 @@ private fun AudioScreen(
checkOnClick: () -> Unit, checkOnClick: () -> Unit,
) { ) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(text = stringResource(R.string.audio_settings), fontWeight = FontWeight.Bold)
text = stringResource(R.string.audio_settings),
fontWeight = FontWeight.Bold
)
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -136,10 +127,7 @@ private fun AudioScreen(
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
actions = { actions = {
IconButton(onClick = checkOnClick) { IconButton(onClick = checkOnClick) {
Icon( Icon(imageVector = Icons.Filled.Check, contentDescription = "Check")
imageVector = Icons.Filled.Check,
contentDescription = "Check"
)
} }
}, },
) )
@@ -319,9 +307,8 @@ private fun ToneCountry() {
isDropDownExpanded.value = false isDropDownExpanded.value = false
}) { }) {
countryNames.forEachIndexed { index, name -> countryNames.forEachIndexed { index, name ->
DropdownMenuItem(text = { DropdownMenuItem(
Text(text = name) text = { Text(text = name) },
},
onClick = { onClick = {
isDropDownExpanded.value = false isDropDownExpanded.value = false
itemPosition.intValue = index itemPosition.intValue = index
@@ -355,8 +342,7 @@ private fun SpeakerPhone() {
var speakerPhone by remember { mutableStateOf(oldSpeakerPhone) } var speakerPhone by remember { mutableStateOf(oldSpeakerPhone) }
Switch( Switch(
checked = speakerPhone, checked = speakerPhone,
onCheckedChange = { onCheckedChange = { speakerPhone = it
speakerPhone = it
newSpeakerPhone = speakerPhone newSpeakerPhone = speakerPhone
} }
) )
@@ -409,9 +395,8 @@ private fun CallVolume() {
isDropDownExpanded.value = false isDropDownExpanded.value = false
}) { }) {
volNames.forEachIndexed { index, vol -> volNames.forEachIndexed { index, vol ->
DropdownMenuItem(text = { DropdownMenuItem(
Text(text = vol) text = { Text(text = vol) },
},
onClick = { onClick = {
isDropDownExpanded.value = false isDropDownExpanded.value = false
itemPosition.intValue = index itemPosition.intValue = index
@@ -440,8 +425,7 @@ private fun MicGain() {
OutlinedTextField( OutlinedTextField(
value = micGain, value = micGain,
placeholder = { Text(microphoneGainTitle) }, placeholder = { Text(microphoneGainTitle) },
onValueChange = { onValueChange = { micGain = it
micGain = it
newMicGain = micGain newMicGain = micGain
}, },
modifier = Modifier modifier = Modifier
@@ -626,7 +610,8 @@ private fun checkOnClick(ctx: Context): Result {
Api.module_unload("augain") Api.module_unload("augain")
Config.removeVariableValue("module", "augain.so") Config.removeVariableValue("module", "augain.so")
Config.replaceVariable("augain", "1.0") Config.replaceVariable("augain", "1.0")
} else { }
else {
if (oldMicGain == "1.0") { if (oldMicGain == "1.0") {
if (Api.module_load("augain") != 0) { if (Api.module_load("augain") != 0) {
alertTitle.value = ctx.getString(R.string.error) alertTitle.value = ctx.getString(R.string.error)
@@ -11,12 +11,12 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -108,17 +108,11 @@ private fun BlockedScreen(navController: NavController, request: String, aor: St
} }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
TopAppBar(navController, account, request, blocked) TopAppBar(navController, account, request, blocked)
} }
}, },
@@ -187,10 +181,7 @@ private fun TopAppBar(
IconButton( IconButton(
onClick = { expanded = !expanded } onClick = { expanded = !expanded }
) { ) {
Icon( Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu")
imageVector = Icons.Filled.Menu,
contentDescription = "Menu",
)
} }
CustomElements.DropdownMenu( CustomElements.DropdownMenu(
expanded, expanded,
@@ -237,9 +228,7 @@ private fun BlockedContent(
private fun Account(account: Account) { private fun Account(account: Account) {
Text( Text(
text = stringResource(R.string.account) + " " + account.text(), text = stringResource(R.string.account) + " " + account.text(),
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
.fillMaxWidth()
.padding(top = 8.dp),
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -292,18 +281,17 @@ private fun Blocked(ctx: Context, navController: NavController, blocked: Mutable
} }
) )
) { ) {
Text(text = "\u2022", Text(
text = "\u2022",
modifier = Modifier.padding(start = 8.dp, end = 4.dp), modifier = Modifier.padding(start = 8.dp, end = 4.dp),
fontSize = 18.sp) fontSize = 18.sp)
Text(
Text(text = peerUri.replace("sip:", ""), text = peerUri.replace("sip:", ""),
fontSize = 18.sp, fontSize = 18.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
val calendar = GregorianCalendar() val calendar = GregorianCalendar()
calendar.timeInMillis = blocked.timeStamp calendar.timeInMillis = blocked.timeStamp
Text( Text(
@@ -324,9 +312,8 @@ private fun loadBlocked(request: String, aor: String): MutableList<Blocked> {
val res = mutableListOf<Blocked>() val res = mutableListOf<Blocked>()
for (i in BaresipService.blocked.indices.reversed()) { for (i in BaresipService.blocked.indices.reversed()) {
val b = BaresipService.blocked[i] val b = BaresipService.blocked[i]
if (b.aor == aor && b.request == request) { if (b.aor == aor && b.request == request)
res.add(Blocked("", b.peerUri, "", b.timeStamp)) res.add(Blocked("", b.peerUri, "", b.timeStamp))
}
} }
Log.d(TAG, "Loaded ${res.size} blocked $request requests") Log.d(TAG, "Loaded ${res.size} blocked $request requests")
return res return res
@@ -18,13 +18,13 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -80,6 +80,7 @@ import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.text.DateFormat import java.text.DateFormat
import java.util.GregorianCalendar import java.util.GregorianCalendar
import kotlin.time.Duration.Companion.milliseconds
fun NavGraphBuilder.callDetailsScreenRoute(navController: NavController, viewModel: ViewModel) { fun NavGraphBuilder.callDetailsScreenRoute(navController: NavController, viewModel: ViewModel) {
composable("call_details") { _ -> composable("call_details") { _ ->
@@ -93,15 +94,11 @@ fun NavGraphBuilder.callDetailsScreenRoute(navController: NavController, viewMod
private fun CallDetailsScreen(navController: NavController, callRow: CallRow) { private fun CallDetailsScreen(navController: NavController, callRow: CallRow) {
val detailsState = remember { callRow.details.toMutableStateList() } val detailsState = remember { callRow.details.toMutableStateList() }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
@@ -252,22 +249,26 @@ private fun startTime(detail: Details, onDelete: (Details) -> Unit): String {
val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) { val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM) val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
stringResource(R.string.today) + " " + fmt.format(stopTime.time) stringResource(R.string.today) + " " + fmt.format(stopTime.time)
} else { }
else {
val fmt = DateFormat.getDateTimeInstance() val fmt = DateFormat.getDateTimeInstance()
fmt.format(stopTime.time) fmt.format(stopTime.time)
} }
if (startTime == GregorianCalendar(0, 0, 0)) { if (startTime == GregorianCalendar(0, 0, 0)) {
startTimeText = stopText startTimeText = stopText
durationText = "?" durationText = "?"
} else { }
else {
if (startTime == null || detail.direction == CALL_DOWN_BLUE) { if (startTime == null || detail.direction == CALL_DOWN_BLUE) {
startTimeText = stopText startTimeText = stopText
durationText = "" durationText = ""
} else { }
else {
val startText = if (DateUtils.isToday(startTime.timeInMillis)) { val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM) val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
stringResource(R.string.today) + " " + fmt.format(startTime.time) stringResource(R.string.today) + " " + fmt.format(startTime.time)
} else { }
else {
val fmt = DateFormat.getDateTimeInstance() val fmt = DateFormat.getDateTimeInstance()
fmt.format(startTime.time) fmt.format(startTime.time)
} }
@@ -294,9 +295,7 @@ private fun startTime(detail: Details, onDelete: (Details) -> Unit): String {
text = startTimeText, text = startTimeText,
modifier = Modifier.combinedClickable( modifier = Modifier.combinedClickable(
onClick = {}, onClick = {},
onLongClick = { onLongClick = { showDialog.value = true }
showDialog.value = true
}
) )
) )
@@ -313,7 +312,7 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
val mediaPlayer = remember { MediaPlayer() } val mediaPlayer = remember { MediaPlayer() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// 1. Setup the File Saver Launcher // 1. Set up the File Saver Launcher
val saveLauncher = rememberLauncherForActivityResult( val saveLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("audio/x-wav") contract = ActivityResultContracts.CreateDocument("audio/x-wav")
) { uri -> ) { uri ->
@@ -335,12 +334,12 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} else { }
else
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(ctx, "Source file not found", Toast.LENGTH_SHORT) Toast.makeText(ctx, "Source file not found", Toast.LENGTH_SHORT)
.show() .show()
} }
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to save file: $e") Log.e(TAG, "Failed to save file: $e")
@@ -381,16 +380,14 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
) )
val hasRecording = detail.recording.isNotEmpty() && detail.recording[0].isNotEmpty() val hasRecording = detail.recording.isNotEmpty() && detail.recording[0].isNotEmpty()
if (hasRecording) { if (hasRecording)
Text( Text(
text = durationText, text = durationText,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
modifier = Modifier modifier = Modifier
.padding(end = 12.dp) .padding(end = 12.dp)
.combinedClickable( .combinedClickable(
onLongClick = { onLongClick = { showDownloadDialog.value = true },
showDownloadDialog.value = true
},
onClick = { onClick = {
if (!mediaPlayer.isPlaying) { if (!mediaPlayer.isPlaying) {
mediaPlayer.reset() mediaPlayer.reset()
@@ -425,15 +422,15 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
) )
finalFile = fallbackMerged finalFile = fallbackMerged
// Update state to match reality by creating a new List // Update state to match reality by creating a new List
detail.recording = detail.recording = listOf(fallbackMerged.absolutePath, "")
listOf(fallbackMerged.absolutePath, "") }
} else { else
Log.e( Log.e(
TAG, TAG,
"Raw file missing and fallback not found: ${currentRecording[0]}" "Raw file missing and fallback not found: ${currentRecording[0]}"
) )
} }
} else { else {
// Normal Raw processing // Normal Raw processing
val mergedFileName = val mergedFileName =
"merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav" "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
@@ -442,20 +439,16 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
mergedFileName mergedFileName
) )
if (mergedFile.exists()) { if (mergedFile.exists()) {
Log.d( Log.d(TAG, "Using already merged file: ${mergedFile.name}")
TAG,
"Using already merged file: ${mergedFile.name}"
)
finalFile = mergedFile finalFile = mergedFile
} else { }
else
if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile))
finalFile = mergedFile finalFile = mergedFile
}
// If merge successful, update state and delete originals // If merge successful, update state and delete originals
if (finalFile != null && finalFile.exists()) { if (finalFile != null && finalFile.exists()) {
detail.recording = detail.recording = listOf(finalFile.absolutePath, "")
listOf(finalFile.absolutePath, "")
try { try {
if (fileIn.exists()) fileIn.delete() if (fileIn.exists()) fileIn.delete()
if (fileOut.exists()) fileOut.delete() if (fileOut.exists()) fileOut.delete()
@@ -468,21 +461,27 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
} }
} }
} }
} else if (currentIsMerged) { }
val f = File(currentRecording[0]) else {
if (f.exists()) { if (currentIsMerged) {
Log.d(TAG, "Using already merged file: ${currentRecording[0]}") val f = File(currentRecording[0])
finalFile = f if (f.exists()) {
} else { Log.d(
Log.e( TAG,
TAG, "Using already merged file: ${currentRecording[0]}"
"Merged file record exists but file is missing: ${currentRecording[0]}" )
) finalFile = f
}
else
Log.e(
TAG,
"Merged file record exists but file is missing: ${currentRecording[0]}"
)
} }
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (finalFile != null && finalFile.exists()) { if (finalFile != null && finalFile.exists())
try { try {
mediaPlayer.apply { mediaPlayer.apply {
setAudioAttributes( setAudioAttributes(
@@ -511,15 +510,15 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} else { else
Toast.makeText( Toast.makeText(
ctx, "Failed to process audio file", ctx, "Failed to process audio file",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
}
} }
} }
} else { }
else {
mediaPlayer.stop() mediaPlayer.stop()
mediaPlayer.reset() mediaPlayer.reset()
showPlaybackDialog.value = false showPlaybackDialog.value = false
@@ -527,9 +526,8 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
} }
) )
) )
} else { else
Text(text = durationText, modifier = Modifier.padding(end = 12.dp)) Text(text = durationText, modifier = Modifier.padding(end = 12.dp))
}
} }
@Composable @Composable
@@ -549,24 +547,18 @@ private fun PlaybackDialog(
if (mediaPlayer.isPlaying) { if (mediaPlayer.isPlaying) {
val duration = mediaPlayer.duration val duration = mediaPlayer.duration
totalDurationText = DateUtils.formatElapsedTime(duration / 1000L) totalDurationText = DateUtils.formatElapsedTime(duration / 1000L)
while (mediaPlayer.isPlaying) { while (mediaPlayer.isPlaying) {
val current = mediaPlayer.currentPosition val current = mediaPlayer.currentPosition
currentProgress = if (duration > 0) current.toFloat() / duration.toFloat() else 0f currentProgress = if (duration > 0) current.toFloat() / duration.toFloat() else 0f
currentPositionText = DateUtils.formatElapsedTime(current / 1000L) currentPositionText = DateUtils.formatElapsedTime(current / 1000L)
delay(100) // Poll every 100ms delay(100.milliseconds) // Poll every 100ms
} }
} }
} }
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = { onStop() }, // If user clicks outside, stop playback
// If user clicks outside, stop playback title = { Text(text = stringResource(R.string.playing_recording)) },
onStop()
},
title = {
Text(text = stringResource(R.string.playing_recording))
},
text = { text = {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -587,9 +579,7 @@ private fun PlaybackDialog(
} }
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(onClick = { onStop() }) {
onClick = { onStop() }
) {
Text(stringResource(R.string.stop)) Text(stringResource(R.string.stop))
} }
} }
@@ -14,13 +14,13 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -121,15 +121,11 @@ private fun CallsScreen(navController: NavController, viewModel: ViewModel, aor:
} }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
TopAppBar(navController, ua, callHistory) TopAppBar(navController, ua, callHistory)
} }
}, },
@@ -203,10 +199,7 @@ private fun TopAppBar(navController: NavController, ua: UserAgent, callHistory:
IconButton( IconButton(
onClick = { expanded = !expanded } onClick = { expanded = !expanded }
) { ) {
Icon( Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu")
imageVector = Icons.Filled.Menu,
contentDescription = "Menu",
)
} }
CustomElements.DropdownMenu( CustomElements.DropdownMenu(
expanded, expanded,
@@ -353,15 +346,12 @@ private fun Calls(
) )
secondButtonText.value = ctx.getString(R.string.call) secondButtonText.value = ctx.getString(R.string.call)
secondAction.value = { secondAction.value = {
if (ua.account.isMobile && ua.status != circleGreen.getValue( if (ua.account.isMobile && ua.status != circleGreen.getValue(colorblind)) {
colorblind
)
) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = alertMessage.value = ctx.getString(R.string.airplane_mode)
ctx.getString(R.string.airplane_mode)
showAlert.value = true showAlert.value = true
} else { }
else {
handleIntent(ctx, viewModel, intent, "call") handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") { navController.navigate("main") {
popUpTo("main") popUpTo("main")
@@ -377,16 +367,20 @@ private fun Calls(
alertMessage.value = alertMessage.value =
ctx.getString(R.string.airplane_mode) ctx.getString(R.string.airplane_mode)
showAlert.value = true showAlert.value = true
} else if (!Utils.isDefaultSmsApp(ctx)) { }
alertTitle.value = ctx.getString(R.string.notice) else
alertMessage.value = if (!Utils.isDefaultSmsApp(ctx)) {
ctx.getString(R.string.enable_default_messaging) alertTitle.value = ctx.getString(R.string.notice)
showAlert.value = true alertMessage.value =
} else { ctx.getString(R.string.enable_default_messaging)
showAlert.value = true
}
else {
handleIntent(ctx, viewModel, intent, "message") handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp() navController.navigateUp()
} }
} else { }
else {
handleIntent(ctx, viewModel, intent, "message") handleIntent(ctx, viewModel, intent, "message")
navController.navigateUp() navController.navigateUp()
} }
@@ -400,13 +394,11 @@ private fun Calls(
try { try {
ctx.startActivity(emailIntent) ctx.startActivity(emailIntent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e( Log.e(TAG, "Failed to start email activity: ${e.message}")
TAG,
"Failed to start email activity: ${e.message}"
)
} }
} }
} else }
else
lastButtonText.value = "" lastButtonText.value = ""
showDialog.value = true showDialog.value = true
} }
@@ -426,13 +418,9 @@ private fun Calls(
) )
secondButtonText.value = "" secondButtonText.value = ""
thirdButtonText.value = ctx.getString(R.string.copy_uri) thirdButtonText.value = ctx.getString(R.string.copy_uri)
thirdAction.value = { thirdAction.value = { Utils.copyToClipboard(ctx, peerUri) }
Utils.copyToClipboard(ctx, peerUri)
}
lastButtonText.value = ctx.getString(R.string.delete) lastButtonText.value = ctx.getString(R.string.delete)
lastAction.value = { lastAction.value = { removeFromHistory(callHistory, callRow) }
removeFromHistory(callHistory, callRow)
}
} }
else { else {
message.value = String.format( message.value = String.format(
@@ -446,13 +434,10 @@ private fun Calls(
navController.navigate("contact/$uri/new") navController.navigate("contact/$uri/new")
} }
thirdButtonText.value = ctx.getString(R.string.copy_uri) thirdButtonText.value = ctx.getString(R.string.copy_uri)
thirdAction.value = { thirdAction.value = { Utils.copyToClipboard(ctx, peerUri)
Utils.copyToClipboard(ctx, peerUri)
} }
lastButtonText.value = ctx.getString(R.string.delete) lastButtonText.value = ctx.getString(R.string.delete)
lastAction.value = { lastAction.value = { removeFromHistory(callHistory, callRow) }
removeFromHistory(callHistory, callRow)
}
} }
showDialog.value = true showDialog.value = true
} }
@@ -509,7 +494,8 @@ private fun Calls(
} }
if (count > 3) if (count > 3)
Text("...", color = MaterialTheme.colorScheme.onBackground) Text("...", color = MaterialTheme.colorScheme.onBackground)
Text(text = Utils.friendlyUri(ctx, peerUri, ua.account), Text(
text = Utils.friendlyUri(ctx, peerUri, ua.account),
modifier = Modifier.padding(start = 8.dp), modifier = Modifier.padding(start = 8.dp),
fontSize = 18.sp, fontSize = 18.sp,
maxLines = 1, maxLines = 1,
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
@@ -22,7 +21,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -136,14 +135,10 @@ private fun ChatScreen(
} }
} }
lifecycleOwner.lifecycle.addObserver(observer) lifecycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
lifecycleOwner.lifecycle.removeObserver(observer)
}
} }
var areMessagesLoaded by remember(aor, peerUri) { var areMessagesLoaded by remember(aor, peerUri) { mutableStateOf(false) }
mutableStateOf(false)
}
val reloadMessages = { val reloadMessages = {
Log.d(TAG, "Reloading messages for $aor peer $peerUri") Log.d(TAG, "Reloading messages for $aor peer $peerUri")
@@ -175,16 +170,11 @@ private fun ChatScreen(
} }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding(),
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column(modifier = Modifier Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
.fillMaxWidth() Spacer(Modifier.statusBarsPadding())
.background(MaterialTheme.colorScheme.background)
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
TopAppBar(ctx, navController, viewModel, account, peerUri) TopAppBar(ctx, navController, viewModel, account, peerUri)
} }
}, },
@@ -199,7 +189,8 @@ private fun ChatScreen(
}, },
content = { contentPadding -> content = { contentPadding ->
if (areMessagesLoaded) if (areMessagesLoaded)
ChatContent(ctx, ChatContent(
ctx,
navController, navController,
contentPadding, contentPadding,
account, peerUri, account, peerUri,
@@ -223,8 +214,7 @@ private fun TopAppBar(
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
text = format(ctx.getString(R.string.chat_with), text = format(ctx.getString(R.string.chat_with), Utils.friendlyUri(ctx, peerUri, account)),
Utils.friendlyUri(ctx, peerUri, account)),
fontSize = 22.sp, fontSize = 22.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -237,10 +227,7 @@ private fun TopAppBar(
), ),
navigationIcon = { navigationIcon = {
IconButton(onClick = { backAction(navController, account, peerUri) }) { IconButton(onClick = { backAction(navController, account, peerUri) }) {
Icon( Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
} }
}, },
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
@@ -290,10 +277,7 @@ private fun TopAppBar(
Log.w(TAG, "Call button onClick listener did not find UA for $aor") Log.w(TAG, "Call button onClick listener did not find UA for $aor")
} }
) { ) {
Icon( Icon(imageVector = Icons.Outlined.Email, contentDescription = "Email")
imageVector = Icons.Outlined.Email,
contentDescription = "Email",
)
} }
} }
) )
@@ -310,9 +294,7 @@ private fun ChatContent(
onMessageDeleted: () -> Unit onMessageDeleted: () -> Unit
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(contentPadding),
.fillMaxWidth()
.padding(contentPadding),
verticalArrangement = Arrangement.Bottom verticalArrangement = Arrangement.Bottom
) { ) {
Account(ctx, account) Account(ctx, account)
@@ -325,9 +307,7 @@ private fun ChatContent(
private fun Account(ctx: Context, account: Account) { private fun Account(ctx: Context, account: Account) {
Text( Text(
text = ctx.getString(R.string.account) + " " + account.text(), text = ctx.getString(R.string.account) + " " + account.text(),
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp),
.fillMaxWidth()
.padding(top = 8.dp, bottom = 8.dp),
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -368,11 +348,8 @@ private fun Messages(
LaunchedEffect(messages) { LaunchedEffect(messages) {
// Scroll to the bottom when new messages are added // Scroll to the bottom when new messages are added
if (messages.isNotEmpty()) { if (messages.isNotEmpty())
coroutineScope.launch { coroutineScope.launch { lazyListState.scrollToItem(0) }
lazyListState.scrollToItem(0)
}
}
} }
LazyColumn( LazyColumn(
@@ -420,9 +397,7 @@ private fun Messages(
peerUri peerUri
) )
secondButtonText.value = ctx.getString(R.string.add_contact) secondButtonText.value = ctx.getString(R.string.add_contact)
secondAction.value = { secondAction.value = { navController.navigate("contact/$peerUri/new") }
navController.navigate("contact/$peerUri/new")
}
lastButtonText.value = ctx.getString(R.string.delete) lastButtonText.value = ctx.getString(R.string.delete)
lastAction.value = { lastAction.value = {
message.delete() message.delete()
@@ -588,14 +563,16 @@ private fun NewMessage(
if (ua.status != circleGreen.getValue(colorblind)) { if (ua.status != circleGreen.getValue(colorblind)) {
dialogMessage.value = ctx.getString(R.string.airplane_mode) dialogMessage.value = ctx.getString(R.string.airplane_mode)
showDialog.value = true showDialog.value = true
} else { }
else {
val destination = Utils.uriUserPart(peerUri) val destination = Utils.uriUserPart(peerUri)
if (Utils.sendSms(ctx, destination, msgText)) { if (Utils.sendSms(ctx, destination, msgText)) {
msg.direction = MESSAGE_UP msg.direction = MESSAGE_UP
newMessage.value = TextFieldValue("") newMessage.value = TextFieldValue("")
viewModel.updateAorPeerMessage(aor, peerUri, "") viewModel.updateAorPeerMessage(aor, peerUri, "")
keyboardController?.hide() keyboardController?.hide()
} else { }
else {
Toast.makeText( Toast.makeText(
ctx, "${ctx.getString(R.string.message_failed)}!", ctx, "${ctx.getString(R.string.message_failed)}!",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
@@ -604,38 +581,36 @@ private fun NewMessage(
msg.responseReason = ctx.getString(R.string.message_failed) msg.responseReason = ctx.getString(R.string.message_failed)
} }
} }
} else { }
if (Utils.isTelUri(peerUri)) else {
if (Utils.isTelUri(peerUri)) {
if (ua.account.telProvider == "") { if (ua.account.telProvider == "") {
dialogMessage.value = String.format( dialogMessage.value = String.format(
ctx.getString(R.string.no_telephony_provider), ctx.getString(R.string.no_telephony_provider),
Utils.plainAor(aor) Utils.plainAor(aor)
) )
showDialog.value = true showDialog.value = true
} else {
msgUri = Utils.telToSip(peerUri, ua.account)
} }
else
msgUri = Utils.telToSip(peerUri, ua.account)
}
else else
msgUri = peerUri msgUri = peerUri
if (msgUri != "") if (msgUri != "") {
if (Api.message_send( if (Api.message_send(ua.uap, msgUri, msgText, time.toString()) != 0) {
ua.uap,
msgUri,
msgText,
time.toString()
) != 0
) {
Toast.makeText( Toast.makeText(
ctx, "${ctx.getString(R.string.message_failed)}!", ctx, "${ctx.getString(R.string.message_failed)}!",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
msg.direction = MESSAGE_UP_FAIL msg.direction = MESSAGE_UP_FAIL
msg.responseReason = ctx.getString(R.string.message_failed) msg.responseReason = ctx.getString(R.string.message_failed)
} else { }
else {
newMessage.value = TextFieldValue("") newMessage.value = TextFieldValue("")
viewModel.updateAorPeerMessage(aor, peerUri, "") viewModel.updateAorPeerMessage(aor, peerUri, "")
keyboardController?.hide() keyboardController?.hide()
} }
}
} }
} }
}, },
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -23,7 +22,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -127,23 +126,15 @@ private fun ChatsScreen(navController: NavController, aor: String) {
refreshTrigger++ refreshTrigger++
} }
lifecycleOwner.lifecycle.addObserver(observer) lifecycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
lifecycleOwner.lifecycle.removeObserver(observer)
}
} }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding(),
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
TopAppBar(navController, account, uaMessages) TopAppBar(navController, account, uaMessages)
} }
}, },
@@ -188,10 +179,7 @@ private fun TopAppBar(
), ),
navigationIcon = { navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) { IconButton(onClick = { navController.navigateUp() }) {
Icon( Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
} }
}, },
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
@@ -199,10 +187,7 @@ private fun TopAppBar(
IconButton( IconButton(
onClick = { menuExpanded = !menuExpanded } onClick = { menuExpanded = !menuExpanded }
) { ) {
Icon( Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu")
imageVector = Icons.Filled.Menu,
contentDescription = "Menu",
)
} }
DropdownMenu ( DropdownMenu (
expanded = menuExpanded, expanded = menuExpanded,
@@ -218,9 +203,8 @@ private fun TopAppBar(
} }
showDialog.value = true showDialog.value = true
} }
blocked -> { blocked ->
navController.navigate("blocked/message/${account.aor}") navController.navigate("blocked/message/${account.aor}")
}
} }
} }
) )
@@ -237,9 +221,7 @@ private fun ChatsContent(
uaMessages: MutableState<List<Message>> uaMessages: MutableState<List<Message>>
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(contentPadding),
.fillMaxWidth()
.padding(contentPadding),
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top
) { ) {
Account(account) Account(account)
@@ -308,7 +290,6 @@ private fun Chats(
else else
CustomElements.TextAvatar(contact.name, contact.color) CustomElements.TextAvatar(contact.name, contact.color)
} }
is Contact.AndroidContact -> { is Contact.AndroidContact -> {
val thumbNailUri = contact.thumbnailUri val thumbNailUri = contact.thumbnailUri
if (thumbNailUri != null) if (thumbNailUri != null)
@@ -321,7 +302,6 @@ private fun Chats(
else else
CustomElements.TextAvatar(contact.name, contact.color) CustomElements.TextAvatar(contact.name, contact.color)
} }
null -> { null -> {
Icon( Icon(
imageVector = Icons.Filled.AccountCircle, imageVector = Icons.Filled.AccountCircle,
@@ -332,20 +312,16 @@ private fun Chats(
} }
} }
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
val buttonShape = if (message.direction == MESSAGE_DOWN) { val buttonShape = if (message.direction == MESSAGE_DOWN)
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp) RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
} else { else
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp) RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp)
} val borderStroke = if (account.unreadMessages && Message.unreadMessagesFromPeer(aor, message.peerUri))
val borderStroke = if (account.unreadMessages && Message.unreadMessagesFromPeer(aor, message.peerUri)) {
BorderStroke(width = 2.dp, color = MaterialTheme.colorScheme.error) BorderStroke(width = 2.dp, color = MaterialTheme.colorScheme.error)
} else { else
null null
}
CustomElements.Button( CustomElements.Button(
onClick = { onClick = { navController.navigate("chat/${aor}/${message.peerUri}") },
navController.navigate("chat/${aor}/${message.peerUri}")
},
onLongClick = { onLongClick = {
val peerName = Utils.friendlyUri(ctx, message.peerUri, account, includeLabel = false) val peerName = Utils.friendlyUri(ctx, message.peerUri, account, includeLabel = false)
val peerNameWithLabel = Utils.friendlyUri(ctx, message.peerUri, account) val peerNameWithLabel = Utils.friendlyUri(ctx, message.peerUri, account)
@@ -361,20 +337,17 @@ private fun Chats(
lastAction.value = { lastAction.value = {
deleteMessages(uaMessages, account, message.peerUri) deleteMessages(uaMessages, account, message.peerUri)
} }
} else { }
else {
dialogMessage.value = dialogMessage.value =
String.format( String.format(
ctx.getString(R.string.long_chat_question), ctx.getString(R.string.long_chat_question),
peerName peerName
) )
secondButtonText.value = ctx.getString(R.string.delete) secondButtonText.value = ctx.getString(R.string.delete)
secondAction.value = { secondAction.value = { deleteMessages(uaMessages, account, message.peerUri) }
deleteMessages(uaMessages, account, message.peerUri)
}
lastButtonText.value = ctx.getString(R.string.add_contact) lastButtonText.value = ctx.getString(R.string.add_contact)
lastAction.value = { lastAction.value = { navController.navigate("contact/${message.peerUri}/new") }
navController.navigate("contact/${message.peerUri}/new")
}
} }
showDialog.value = true showDialog.value = true
}, },
@@ -497,10 +470,7 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
var newPeer by remember { mutableStateOf("") } var newPeer by remember { mutableStateOf("") }
Column( Column(horizontalAlignment = Alignment.Start, modifier = Modifier.weight(1f)) {
horizontalAlignment = Alignment.Start,
modifier = Modifier.weight(1f)
) {
if (showSuggestions && filteredSuggestions.isNotEmpty()) { if (showSuggestions && filteredSuggestions.isNotEmpty()) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -511,8 +481,7 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
) )
.animateContentSize() .animateContentSize()
) { ) {
Box(modifier = Modifier.fillMaxWidth().heightIn(max = 150.dp) Box(modifier = Modifier.fillMaxWidth().heightIn(max = 150.dp)) {
) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -553,9 +522,9 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
onValueChange = { onValueChange = {
newPeer = it newPeer = it
showSuggestions = newPeer.length > 1 showSuggestions = newPeer.length > 1
filteredSuggestions = if (it.isEmpty()) { filteredSuggestions = if (it.isEmpty())
emptyList() emptyList()
} else { else {
val normalizedInput = Utils.unaccent(it) val normalizedInput = Utils.unaccent(it)
suggestions suggestions
.filter { suggestion -> .filter { suggestion ->
@@ -609,7 +578,8 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
} else { } else {
makeChat(ctx, navController, account, peerText) makeChat(ctx, navController, account, peerText)
} }
} else if (uris.size == 1) }
else if (uris.size == 1)
makeChat(ctx, navController, account, uris[0].uri) makeChat(ctx, navController, account, uris[0].uri)
else { else {
items.value = uris.map { it.label.ifEmpty { it.uri.substringAfter(":") } } items.value = uris.map { it.label.ifEmpty { it.uri.substringAfter(":") } }
@@ -656,9 +626,7 @@ private fun loadMessages(account: Account) : List<Message> {
private fun deleteMessages(uaMessages: MutableState<List<Message>>, account: Account, peerUri: String) { private fun deleteMessages(uaMessages: MutableState<List<Message>>, account: Account, peerUri: String) {
val updatedMessages = BaresipService.messages.toMutableList() val updatedMessages = BaresipService.messages.toMutableList()
updatedMessages.removeAll { updatedMessages.removeAll { it.aor == account.aor && (peerUri == "" || it.peerUri == peerUri) }
it.aor == account.aor && (peerUri == "" || it.peerUri == peerUri)
}
BaresipService.messages = updatedMessages.toList() BaresipService.messages = updatedMessages.toList()
Message.save() Message.save()
uaMessages.value = loadMessages(account) uaMessages.value = loadMessages(account)
@@ -5,13 +5,14 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -98,16 +99,14 @@ private fun CodecsScreen(
val codecs = remember { mutableStateListOf<Codec>() } val codecs = remember { mutableStateListOf<Codec>() }
LaunchedEffect(acc, media) { LaunchedEffect(acc, media) {
val allCodecs: List<String> = if (media == "audio") { val allCodecs: List<String> = if (media == "audio")
Api.audio_codecs().split(",") Api.audio_codecs().split(",")
} else { else
Api.video_codecs().split(",").distinct() Api.video_codecs().split(",").distinct()
} val accCodecs: List<String> = if (media == "audio")
val accCodecs: List<String> = if (media == "audio") {
acc.audioCodec acc.audioCodec
} else { else
acc.videoCodec acc.videoCodec
}
val currentCodecs = mutableListOf<Codec>() val currentCodecs = mutableListOf<Codec>()
for (codec in accCodecs) for (codec in accCodecs)
currentCodecs.add(Codec(codec, mutableStateOf(true))) currentCodecs.add(Codec(codec, mutableStateOf(true)))
@@ -119,17 +118,11 @@ private fun CodecsScreen(
} }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
@@ -146,7 +139,6 @@ private fun CodecsScreen(
titleContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary actionIconContentColor = MaterialTheme.colorScheme.onPrimary
), ),
windowInsets = WindowInsets(0, 0, 0, 0),
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon( Icon(
@@ -155,24 +147,18 @@ private fun CodecsScreen(
) )
} }
}, },
windowInsets = WindowInsets(0, 0, 0, 0),
actions = { actions = {
IconButton(onClick = { IconButton(
checkOnClick(codecs) onClick = { checkOnClick(codecs) }) {
}) { Icon(imageVector = Icons.Filled.Check, contentDescription = "Check")
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Check"
)
} }
} }
) )
} }
}, },
content = { contentPadding -> content = { contentPadding ->
CodecsContent( CodecsContent(contentPadding, codecs)
contentPadding,
codecs
)
}, },
) )
} }
@@ -204,9 +190,7 @@ private fun Codecs(codecs: SnapshotStateList<Codec>) {
) )
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier.padding(end = 4.dp).verticalScrollbar(state = draggableState.listState),
.padding(end = 4.dp)
.verticalScrollbar(state = draggableState.listState),
state = draggableState.listState, state = draggableState.listState,
contentPadding = PaddingValues(start = 12.dp, end = 12.dp), contentPadding = PaddingValues(start = 12.dp, end = 12.dp),
) { ) {
@@ -236,7 +220,8 @@ private fun Codecs(codecs: SnapshotStateList<Codec>) {
val index = codecs.indexOf(item) val index = codecs.indexOf(item)
codecs.removeAt(index) codecs.removeAt(index)
codecs.add(0, item) codecs.add(0, item)
} else { }
else {
val index = codecs.indexOf(item) val index = codecs.indexOf(item)
codecs.removeAt(index) codecs.removeAt(index)
codecs.add(item) codecs.add(item)
@@ -247,10 +232,7 @@ private fun Codecs(codecs: SnapshotStateList<Codec>) {
}, },
trailingContent = { trailingContent = {
Icon( Icon(
modifier = Modifier.dragHandle( modifier = Modifier.dragHandle(state = draggableState, key = item.name),
state = draggableState,
key = item.name
),
imageVector =Icons.Filled.Reorder, imageVector =Icons.Filled.Reorder,
contentDescription = null contentDescription = null
) )
@@ -26,14 +26,15 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@@ -170,7 +171,8 @@ private fun ContactScreen(
isLoading = false, isLoading = false,
isBaresipContact = true isBaresipContact = true
) )
} else { }
else {
val baresipContact = Contact.baresipContact(uriOrNameArg) val baresipContact = Contact.baresipContact(uriOrNameArg)
val androidContact = Contact.androidContact(uriOrNameArg) val androidContact = Contact.androidContact(uriOrNameArg)
val contact = baresipContact ?: androidContact val contact = baresipContact ?: androidContact
@@ -226,7 +228,8 @@ private fun ContactScreen(
null, null,
tmpAvatarFile = null tmpAvatarFile = null
) )
} else { }
else {
screenState.tmpAvatarFile?.let { tempFile -> screenState.tmpAvatarFile?.let { tempFile ->
if (tempFile.exists()) { if (tempFile.exists()) {
Log.d(TAG, "Back pressed, deleting temp avatar: ${tempFile.name}") Log.d(TAG, "Back pressed, deleting temp avatar: ${tempFile.name}")
@@ -238,16 +241,13 @@ private fun ContactScreen(
} }
val onCheck: () -> Unit = { val onCheck: () -> Unit = {
val result = checkOnClick( val result = checkOnClick(ctx = ctx, currentState = screenState, uriOrNameArg = uriOrNameArg)
ctx = ctx,
currentState = screenState,
uriOrNameArg = uriOrNameArg,
)
if (result) { if (result) {
if (screenState.new) { if (screenState.new) {
navController.previousBackStackEntry?.savedStateHandle?.set("scrollToContact", screenState.name) navController.previousBackStackEntry?.savedStateHandle?.set("scrollToContact", screenState.name)
navController.navigateUp() navController.navigateUp()
} else { }
else {
// Update UI state with saved values // Update UI state with saved values
val contact = Contact.baresipContact(screenState.name)!! val contact = Contact.baresipContact(screenState.name)!!
val avatarFile = File(BaresipService.filesPath, "${contact.id}.png") val avatarFile = File(BaresipService.filesPath, "${contact.id}.png")
@@ -270,26 +270,16 @@ private fun ContactScreen(
} }
} }
val onEdit: () -> Unit = { val onEdit: () -> Unit = { screenState = screenState.copy(isEditing = true) }
screenState = screenState.copy(isEditing = true)
}
BackHandler(enabled = true) { BackHandler(enabled = true) { onBack() }
onBack()
}
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
TopAppBar( TopAppBar(
title = title, title = title,
isEditing = screenState.isEditing, isEditing = screenState.isEditing,
@@ -301,7 +291,7 @@ private fun ContactScreen(
} }
}, },
content = { contentPadding -> content = { contentPadding ->
if (!screenState.isLoading) { if (!screenState.isLoading)
ContactContent( ContactContent(
ctx = ctx, ctx = ctx,
viewModel = viewModel, viewModel = viewModel,
@@ -310,11 +300,10 @@ private fun ContactScreen(
screenState = screenState, screenState = screenState,
onStateChange = { newState -> screenState = newState } onStateChange = { newState -> screenState = newState }
) )
} else { else
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator() CircularProgressIndicator()
} }
}
} }
) )
} }
@@ -340,28 +329,21 @@ private fun TopAppBar(
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon( Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
} }
}, },
actions = { actions = {
if (isEditing) { if (isEditing)
IconButton(onClick = onCheck) { IconButton(onClick = onCheck) {
Icon( Icon(imageVector = Icons.Filled.Check, contentDescription = "Save")
imageVector = Icons.Filled.Check,
contentDescription = "Save"
)
} }
} else if (isBaresipContact) { else if (isBaresipContact)
IconButton(onClick = onEdit) { IconButton(onClick = onEdit) {
Icon( Icon(
imageVector = Icons.Filled.Edit, imageVector = Icons.Filled.Edit,
contentDescription = "Edit" contentDescription = "Edit"
) )
} }
}
} }
) )
} }
@@ -510,9 +492,9 @@ private fun AvatarSection(
val newImageId = System.currentTimeMillis() val newImageId = System.currentTimeMillis()
val tempNewImageFile = File(BaresipService.filesPath, "${newImageId}.png") val tempNewImageFile = File(BaresipService.filesPath, "${newImageId}.png")
if (saveBitmap(rotatedBitmap, tempNewImageFile)) { if (saveBitmap(rotatedBitmap, tempNewImageFile))
onNewAvatarChosen(tempNewImageFile, newImageId) onNewAvatarChosen(tempNewImageFile, newImageId)
} else { else {
Log.e(TAG, "Failed to save processed avatar image") Log.e(TAG, "Failed to save processed avatar image")
if (tempNewImageFile.exists()) Utils.deleteFile(tempNewImageFile) if (tempNewImageFile.exists()) Utils.deleteFile(tempNewImageFile)
} }
@@ -534,24 +516,18 @@ private fun AvatarSection(
.clip(CircleShape) .clip(CircleShape)
.background(if (currentAvatarUri == null) Color(color) else Color.Transparent) .background(if (currentAvatarUri == null) Color(color) else Color.Transparent)
.let { modifier -> .let { modifier ->
if (isEditing) { if (isEditing)
modifier.combinedClickable( modifier.combinedClickable(
onClick = { avatarImagePicker.launch("image/*") }, onClick = { avatarImagePicker.launch("image/*") },
onLongClick = { onAvatarColorChange(Utils.randomColor()) } onLongClick = { onAvatarColorChange(Utils.randomColor()) }
) )
} else { else
modifier modifier
}
} }
) { ) {
if (currentAvatarUri == null) { if (currentAvatarUri == null) {
Box( Box(modifier = Modifier.size(avatarSize.dp), contentAlignment = Alignment.Center) {
modifier = Modifier.size(avatarSize.dp), Canvas(modifier = Modifier.fillMaxSize()) { drawCircle(SolidColor(Color(color))) }
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(SolidColor(Color(color)))
}
val text = if (name.isNotBlank()) name.take(1).uppercase() else "?" val text = if (name.isNotBlank()) name.take(1).uppercase() else "?"
Text(text, fontSize = 72.sp, color = Color.White) Text(text, fontSize = 72.sp, color = Color.White)
} }
@@ -575,9 +551,7 @@ private fun ContactNameSection(name: String, isEditing: Boolean, new: Boolean, o
value = name, value = name,
placeholder = { Text(stringResource(R.string.contact_name)) }, placeholder = { Text(stringResource(R.string.contact_name)) },
onValueChange = onNameChange, onValueChange = onNameChange,
modifier = Modifier modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
.fillMaxWidth()
.focusRequester(focusRequester),
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp),
label = { Text(stringResource(R.string.contact_name)) }, label = { Text(stringResource(R.string.contact_name)) },
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
@@ -588,7 +562,8 @@ private fun ContactNameSection(name: String, isEditing: Boolean, new: Boolean, o
LaunchedEffect(new) { LaunchedEffect(new) {
if (new) focusRequester.requestFocus() if (new) focusRequester.requestFocus()
} }
} else { }
else
Row( Row(
Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp), Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -601,7 +576,6 @@ private fun ContactNameSection(name: String, isEditing: Boolean, new: Boolean, o
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
}
} }
@Composable @Composable
@@ -615,16 +589,10 @@ private fun UrisSection(
) { ) {
val selectedAor by viewModel.selectedAor.collectAsState() val selectedAor by viewModel.selectedAor.collectAsState()
if (isEditing) { if (isEditing)
Column( Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
uris.forEachIndexed { index, contactUri -> uris.forEachIndexed { index, contactUri ->
Row( Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Column (modifier = Modifier.weight(1f)) { Column (modifier = Modifier.weight(1f)) {
OutlinedTextField( OutlinedTextField(
value = contactUri.uri, value = contactUri.uri,
@@ -656,7 +624,7 @@ private fun UrisSection(
) )
) )
} }
if (uris.size > 1) { if (uris.size > 1)
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) { CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
IconButton( IconButton(
onClick = { onClick = {
@@ -674,7 +642,6 @@ private fun UrisSection(
) )
} }
} }
}
} }
} }
IconButton( IconButton(
@@ -692,11 +659,8 @@ private fun UrisSection(
) )
} }
} }
} else { else
Column( Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
uris.forEachIndexed { index, contactUri -> uris.forEachIndexed { index, contactUri ->
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val uri = contactUri.uri val uri = contactUri.uri
@@ -735,11 +699,13 @@ private fun UrisSection(
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.airplane_mode) alertMessage.value = ctx.getString(R.string.airplane_mode)
showAlert.value = true showAlert.value = true
} else if (ua.account.isMobile && !Utils.isDefaultSmsApp(ctx)) { }
else if (ua.account.isMobile && !Utils.isDefaultSmsApp(ctx)) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.enable_default_messaging) alertMessage.value = ctx.getString(R.string.enable_default_messaging)
showAlert.value = true showAlert.value = true
} else { }
else {
val intent = Intent(ctx, MainActivity::class.java) val intent = Intent(ctx, MainActivity::class.java)
intent.putExtra("uap", ua.uap) intent.putExtra("uap", ua.uap)
intent.putExtra("peer", uri) intent.putExtra("peer", uri)
@@ -760,17 +726,20 @@ private fun UrisSection(
} }
// Call Button // Call Button
if (ua != null && (if (uri.startsWith("tel:")) if (ua != null &&
if (uri.startsWith("tel:"))
ua.account.isMobile || ua.account.telProvider != "" ua.account.isMobile || ua.account.telProvider != ""
else else
!ua.account.isMobile)) !ua.account.isMobile
)
IconButton( IconButton(
onClick = { onClick = {
if (ua.account.isMobile && ua.status != circleGreen.getValue(colorblind)) { if (ua.account.isMobile && ua.status != circleGreen.getValue(colorblind)) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.airplane_mode) alertMessage.value = ctx.getString(R.string.airplane_mode)
showAlert.value = true showAlert.value = true
} else { }
else {
val intent = Intent(ctx, MainActivity::class.java) val intent = Intent(ctx, MainActivity::class.java)
intent.putExtra("uap", ua.uap) intent.putExtra("uap", ua.uap)
intent.putExtra("peer", uri) intent.putExtra("peer", uri)
@@ -792,12 +761,11 @@ private fun UrisSection(
} }
} }
} }
}
} }
@Composable @Composable
private fun EmailSection(ctx: Context, email: String, isEditing: Boolean, onEmailChange: (String) -> Unit) { private fun EmailSection(ctx: Context, email: String, isEditing: Boolean, onEmailChange: (String) -> Unit) {
if (isEditing) { if (isEditing)
OutlinedTextField( OutlinedTextField(
value = email, value = email,
placeholder = { Text(stringResource(R.string.email)) }, placeholder = { Text(stringResource(R.string.email)) },
@@ -807,7 +775,7 @@ private fun EmailSection(ctx: Context, email: String, isEditing: Boolean, onEmai
label = { Text(stringResource(R.string.email)) }, label = { Text(stringResource(R.string.email)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
) )
} else if (email.isNotEmpty()) { else if (email.isNotEmpty())
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -846,7 +814,6 @@ private fun EmailSection(ctx: Context, email: String, isEditing: Boolean, onEmai
) )
} }
} }
}
} }
@Composable @Composable
@@ -865,10 +832,7 @@ private fun FavoriteSection(ctx: Context, favorite: Boolean, onFavoriteChange: (
showAlert.value = true showAlert.value = true
}, },
) )
Switch( Switch(checked = favorite, onCheckedChange = onFavoriteChange)
checked = favorite,
onCheckedChange = onFavoriteChange
)
} }
} }
@@ -888,10 +852,7 @@ private fun AndroidSection(ctx: Context, android: Boolean, onAndroidChange: (Boo
showAlert.value = true showAlert.value = true
}, },
) )
Switch( Switch(checked = android, onCheckedChange = onAndroidChange)
checked = android,
onCheckedChange = onAndroidChange
)
} }
} }
@@ -906,7 +867,6 @@ private fun checkOnClick(
if (u == "") continue if (u == "") continue
if (!u.startsWith("sip:") && !u.startsWith("tel:")) if (!u.startsWith("sip:") && !u.startsWith("tel:"))
u = if (Utils.isTelNumber(u)) "tel:$u" else "sip:$u" u = if (Utils.isTelNumber(u)) "tel:$u" else "sip:$u"
if (!Utils.checkUri(u)) { if (!Utils.checkUri(u)) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), u) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), u)
@@ -924,7 +884,10 @@ private fun checkOnClick(
} }
var newName = currentState.name.trim() var newName = currentState.name.trim()
if (newName == "") newName = if (newUris.isNotEmpty()) newUris[0].uri.substringAfter(":") else currentState.email if (newName == "") newName = if (newUris.isNotEmpty())
newUris[0].uri.substringAfter(":")
else
currentState.email
if (!Utils.checkName(newName)) { if (!Utils.checkName(newName)) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_contact), newName) alertMessage.value = String.format(ctx.getString(R.string.invalid_contact), newName)
@@ -934,9 +897,8 @@ private fun checkOnClick(
val alert: Boolean = if (currentState.new) val alert: Boolean = if (currentState.new)
Contact.nameExists(newName, BaresipService.contacts, true) Contact.nameExists(newName, BaresipService.contacts, true)
else { else
(uriOrNameArg != newName) && Contact.nameExists(newName, BaresipService.contacts, false) (uriOrNameArg != newName) && Contact.nameExists(newName, BaresipService.contacts, false)
}
if (alert) { if (alert) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.contact_already_exists), newName) alertMessage.value = String.format(ctx.getString(R.string.contact_already_exists), newName)
@@ -951,7 +913,8 @@ private fun checkOnClick(
if (oldAvatar.exists()) Utils.deleteFile(oldAvatar) if (oldAvatar.exists()) Utils.deleteFile(oldAvatar)
idToUse = currentState.newId idToUse = currentState.newId
} }
} else if (currentState.avatarImageUri == null) { }
else if (currentState.avatarImageUri == null) {
val avatarFile = File(BaresipService.filesPath, "$idToUse.png") val avatarFile = File(BaresipService.filesPath, "$idToUse.png")
if (avatarFile.exists()) Utils.deleteFile(avatarFile) if (avatarFile.exists()) Utils.deleteFile(avatarFile)
} }
@@ -979,12 +942,12 @@ private fun checkOnClick(
Log.e(TAG, "Update of Android favorite failed: ${e.message}") Log.e(TAG, "Update of Android favorite failed: ${e.message}")
} }
} }
} else { }
else
if (currentState.new) if (currentState.new)
Contact.addBaresipContact(contact) Contact.addBaresipContact(contact)
else else
Contact.updateBaresipContact(currentState.id, contact) Contact.updateBaresipContact(currentState.id, contact)
}
return true return true
} }
@@ -1010,7 +973,15 @@ private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f) ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
else -> return bitmap else -> return bitmap
} }
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) val rotatedBitmap = Bitmap.createBitmap(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
matrix,
true
)
bitmap.recycle() bitmap.recycle()
return rotatedBitmap return rotatedBitmap
} }
@@ -1036,11 +1007,10 @@ private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipCont
CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " + CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'" CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
val c: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, null, null) val c: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, null, null)
if (c != null && c.moveToFirst()) { if (c != null && c.moveToFirst())
updateAndroidContact(ctx, c.getLong(0), contact) updateAndroidContact(ctx, c.getLong(0), contact)
} else { else
addAndroidContact(ctx, contact) addAndroidContact(ctx, contact)
}
c?.close() c?.close()
} }
@@ -1053,17 +1023,19 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo
.withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name).build()) .withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name).build())
if (contact.email.isNotEmpty()) { if (contact.email.isNotEmpty())
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) .withValue(Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE)
.withValue(CommonDataKinds.Email.ADDRESS, contact.email) .withValue(CommonDataKinds.Email.ADDRESS, contact.email)
.withValue(CommonDataKinds.Email.TYPE, CommonDataKinds.Email.TYPE_HOME).build()) .withValue(CommonDataKinds.Email.TYPE, CommonDataKinds.Email.TYPE_HOME).build())
}
for (contactUri in contact.uris) { for (contactUri in contact.uris) {
val uri = contactUri.uri val uri = contactUri.uri
val label = contactUri.label val label = contactUri.label
val mimeType = if (uri.startsWith("sip:")) CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE else CommonDataKinds.Phone.CONTENT_ITEM_TYPE val mimeType = if (uri.startsWith("sip:"))
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
else
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
val builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) val builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, mimeType) .withValue(Data.MIMETYPE, mimeType)
@@ -1078,12 +1050,11 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo
} }
if (contact.avatarImage != null) { if (contact.avatarImage != null) {
val photoData = bitmapToPNGByteArray(contact.avatarImage!!) val photoData = bitmapToPNGByteArray(contact.avatarImage!!)
if (photoData != null) { if (photoData != null)
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE) .withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
.withValue(CommonDataKinds.Photo.PHOTO, photoData).build()) .withValue(CommonDataKinds.Photo.PHOTO, photoData).build())
}
} }
try { try {
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
@@ -1095,17 +1066,24 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo
} }
private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) { private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) {
if (contact.email.isNotEmpty()) { if (contact.email.isNotEmpty())
if (updateAndroidEmail(ctx, rawContactId, contact.email) == 0) addAndroidEmail(ctx, rawContactId, contact.email) if (updateAndroidEmail(ctx, rawContactId, contact.email) == 0)
} addAndroidEmail(ctx, rawContactId, contact.email)
for (contactUri in contact.uris) if (updateAndroidUri(ctx, rawContactId, contactUri) == 0) addAndroidUri(ctx, rawContactId, contactUri) for (contactUri in contact.uris)
if (updateAndroidPhoto(ctx, rawContactId, contact.avatarImage) == 0) if (contact.avatarImage != null) addAndroidPhoto(ctx, rawContactId, contact.avatarImage!!) if (updateAndroidUri(ctx, rawContactId, contactUri) == 0)
addAndroidUri(ctx, rawContactId, contactUri)
if (updateAndroidPhoto(ctx, rawContactId, contact.avatarImage) == 0)
if (contact.avatarImage != null)
addAndroidPhoto(ctx, rawContactId, contact.avatarImage!!)
} }
private fun addAndroidUri(ctx: Context, rawContactId: Long, contactUri: Contact.ContactUri) { private fun addAndroidUri(ctx: Context, rawContactId: Long, contactUri: Contact.ContactUri) {
val uri = contactUri.uri val uri = contactUri.uri
val label = contactUri.label val label = contactUri.label
val mimeType = if (uri.startsWith("sip:")) CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE else CommonDataKinds.Phone.CONTENT_ITEM_TYPE val mimeType = if (uri.startsWith("sip:"))
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
else
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
val builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) val builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValue(Data.RAW_CONTACT_ID, rawContactId) .withValue(Data.RAW_CONTACT_ID, rawContactId)
.withValue(Data.MIMETYPE, mimeType) .withValue(Data.MIMETYPE, mimeType)
@@ -1128,7 +1106,10 @@ private fun addAndroidUri(ctx: Context, rawContactId: Long, contactUri: Contact.
private fun updateAndroidUri(ctx: Context, rawContactId: Long, contactUri: Contact.ContactUri): Int { private fun updateAndroidUri(ctx: Context, rawContactId: Long, contactUri: Contact.ContactUri): Int {
val uri = contactUri.uri val uri = contactUri.uri
val label = contactUri.label val label = contactUri.label
val mimeType = if (uri.startsWith("sip:")) CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE else CommonDataKinds.Phone.CONTENT_ITEM_TYPE val mimeType = if (uri.startsWith("sip:"))
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
else
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
val contentValues = ContentValues() val contentValues = ContentValues()
contentValues.put(ContactsContract.Data.DATA1, uri.substringAfter(":")) contentValues.put(ContactsContract.Data.DATA1, uri.substringAfter(":"))
if (mimeType == CommonDataKinds.Phone.CONTENT_ITEM_TYPE) { if (mimeType == CommonDataKinds.Phone.CONTENT_ITEM_TYPE) {
@@ -1137,7 +1118,8 @@ private fun updateAndroidUri(ctx: Context, rawContactId: Long, contactUri: Conta
if (type == CommonDataKinds.Phone.TYPE_CUSTOM) if (type == CommonDataKinds.Phone.TYPE_CUSTOM)
contentValues.put(CommonDataKinds.Phone.LABEL, label) contentValues.put(CommonDataKinds.Phone.LABEL, label)
} }
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and ${ContactsContract.Data.MIMETYPE}='$mimeType'" val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
"${ContactsContract.Data.MIMETYPE}='$mimeType'"
return try { return try {
ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null) ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
} catch (e: Exception) { } catch (e: Exception) {
@@ -1179,7 +1161,8 @@ private fun addAndroidEmail(ctx: Context, rawContactId: Long, email: String) {
private fun updateAndroidEmail(ctx: Context, rawContactId: Long, email: String): Int { private fun updateAndroidEmail(ctx: Context, rawContactId: Long, email: String): Int {
val contentValues = ContentValues() val contentValues = ContentValues()
contentValues.put(CommonDataKinds.Email.ADDRESS, email) contentValues.put(CommonDataKinds.Email.ADDRESS, email)
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and ${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Email.CONTENT_ITEM_TYPE}'" val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Email.CONTENT_ITEM_TYPE}'"
return try { return try {
ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null) ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
} catch (_: Exception) { } catch (_: Exception) {
@@ -1191,7 +1174,8 @@ private fun updateAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitm
val photoBytes = if (photoBits == null) null else bitmapToPNGByteArray(photoBits) val photoBytes = if (photoBits == null) null else bitmapToPNGByteArray(photoBits)
val contentValues = ContentValues() val contentValues = ContentValues()
contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes) contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and ${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'" val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
return try { return try {
ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null) ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
} catch (_: Exception) { } catch (_: Exception) {
@@ -22,15 +22,15 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -128,7 +128,7 @@ private fun ContactsScreen(navController: NavController) {
val vcfExportLauncher = rememberLauncherForActivityResult( val vcfExportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/vcard") contract = ActivityResultContracts.CreateDocument("text/vcard")
) { uri: android.net.Uri? -> ) { uri: android.net.Uri? ->
if (uri != null) { if (uri != null)
try { try {
ctx.contentResolver.openOutputStream(uri)?.use { outputStream -> ctx.contentResolver.openOutputStream(uri)?.use { outputStream ->
val writer = outputStream.bufferedWriter() val writer = outputStream.bufferedWriter()
@@ -155,7 +155,8 @@ private fun ContactsScreen(navController: NavController) {
writer.write("TEL;X-${u.label}:${u.uri.substring(4)}\n") writer.write("TEL;X-${u.label}:${u.uri.substring(4)}\n")
else else
writer.write("TEL:${u.uri.substring(4)}\n") writer.write("TEL:${u.uri.substring(4)}\n")
} else if (u.uri.startsWith("sip:")) { }
else if (u.uri.startsWith("sip:")) {
if (u.label.isNotEmpty()) if (u.label.isNotEmpty())
writer.write("X-SIP;X-${u.label}:${u.uri.substring(4)}\n") writer.write("X-SIP;X-${u.label}:${u.uri.substring(4)}\n")
else else
@@ -171,25 +172,23 @@ private fun ContactsScreen(navController: NavController) {
Log.e(TAG, "Failed to export VCF: ${e.message}") Log.e(TAG, "Failed to export VCF: ${e.message}")
Toast.makeText(ctx, R.string.contact_export_failure, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, R.string.contact_export_failure, Toast.LENGTH_SHORT).show()
} }
}
} }
val vcfImportLauncher = rememberLauncherForActivityResult( val vcfImportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument() contract = ActivityResultContracts.OpenDocument()
) { uri: android.net.Uri? -> ) { uri: android.net.Uri? ->
if (uri != null) { if (uri != null)
try { try {
ctx.contentResolver.openInputStream(uri)?.use { inputStream -> ctx.contentResolver.openInputStream(uri)?.use { inputStream ->
val reader = inputStream.bufferedReader() val reader = inputStream.bufferedReader()
val lines = mutableListOf<String>() val lines = mutableListOf<String>()
reader.forEachLine { line -> reader.forEachLine { line ->
if (line.startsWith(" ") || line.startsWith("\t")) { if (line.startsWith(" ") || line.startsWith("\t")) {
if (lines.isNotEmpty()) { if (lines.isNotEmpty())
lines[lines.size - 1] = lines.last() + line.trim() lines[lines.size - 1] = lines.last() + line.trim()
}
} else {
lines.add(line)
} }
else
lines.add(line)
} }
var name = "" var name = ""
@@ -199,7 +198,7 @@ private fun ContactsScreen(navController: NavController) {
var contactNo = 0 var contactNo = 0
val newBaresipContacts = BaresipService.baresipContacts.value.toMutableList() val newBaresipContacts = BaresipService.baresipContacts.value.toMutableList()
for (line in lines) { for (line in lines)
when { when {
line.startsWith("BEGIN:VCARD", ignoreCase = true) -> { line.startsWith("BEGIN:VCARD", ignoreCase = true) -> {
name = "" name = ""
@@ -241,7 +240,7 @@ private fun ContactsScreen(navController: NavController) {
if (name.isNotEmpty() && (uris.isNotEmpty() || email.isNotEmpty())) { if (name.isNotEmpty() && (uris.isNotEmpty() || email.isNotEmpty())) {
val existingContact = newBaresipContacts.find { it.name == name } val existingContact = newBaresipContacts.find { it.name == name }
val contactId = existingContact?.id ?: (System.currentTimeMillis() + contactNo++) val contactId = existingContact?.id ?: (System.currentTimeMillis() + contactNo++)
if (photoBase64.isNotEmpty()) { if (photoBase64.isNotEmpty())
try { try {
val decodedString = Base64.decode(photoBase64, Base64.DEFAULT) val decodedString = Base64.decode(photoBase64, Base64.DEFAULT)
val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size) val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
@@ -254,17 +253,14 @@ private fun ContactsScreen(navController: NavController) {
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to decode photo for $name: ${e.message}") Log.e(TAG, "Failed to decode photo for $name: ${e.message}")
} }
}
if (existingContact != null) { if (existingContact != null) {
for (u in uris) { for (u in uris)
if (existingContact.uris.none { it.uri == u.uri }) { if (existingContact.uris.none { it.uri == u.uri })
existingContact.uris.add(u) existingContact.uris.add(u)
} if (existingContact.email.isEmpty() && email.isNotEmpty())
}
if (existingContact.email.isEmpty() && email.isNotEmpty()) {
existingContact.email = email existingContact.email = email
} }
} else { else
newBaresipContacts.add( newBaresipContacts.add(
Contact.BaresipContact( Contact.BaresipContact(
name, name,
@@ -275,30 +271,19 @@ private fun ContactsScreen(navController: NavController) {
false false
) )
) )
}
} }
} }
} }
}
BaresipService.baresipContacts.value = newBaresipContacts.toList() BaresipService.baresipContacts.value = newBaresipContacts.toList()
Contact.saveBaresipContacts() Contact.saveBaresipContacts()
Contact.restoreBaresipContacts() Contact.restoreBaresipContacts()
Contact.contactsUpdate() Contact.contactsUpdate()
Toast.makeText( Toast.makeText(ctx, R.string.contact_import_success, Toast.LENGTH_SHORT).show()
ctx,
R.string.contact_import_success,
Toast.LENGTH_SHORT
).show()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to import VCF: ${e.message}") Log.e(TAG, "Failed to import VCF: ${e.message}")
Toast.makeText( Toast.makeText(ctx, R.string.contact_import_failure, Toast.LENGTH_SHORT).show()
ctx,
R.string.contact_import_failure,
Toast.LENGTH_SHORT
).show()
} }
}
} }
val contactNames = remember(BaresipService.contactsMode) { val contactNames = remember(BaresipService.contactsMode) {
@@ -373,11 +358,9 @@ private fun ContactsScreen(navController: NavController) {
contract = ActivityResultContracts.RequestMultiplePermissions() contract = ActivityResultContracts.RequestMultiplePermissions()
) { permissions -> ) { permissions ->
if (permissions[Manifest.permission.READ_CONTACTS] == true && if (permissions[Manifest.permission.READ_CONTACTS] == true &&
permissions[Manifest.permission.WRITE_CONTACTS] == true) { permissions[Manifest.permission.WRITE_CONTACTS] == true)
if (pendingMode.isNotEmpty()) { if (pendingMode.isNotEmpty())
setContactsMode(pendingMode) setContactsMode(pendingMode)
}
}
pendingMode = "" pendingMode = ""
} }
@@ -388,10 +371,7 @@ private fun ContactsScreen(navController: NavController) {
lastButtonText = ok, lastButtonText = ok,
onLastClicked = { onLastClicked = {
requestPermissionsLauncher.launch( requestPermissionsLauncher.launch(
arrayOf( arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
)
) )
} }
) )
@@ -400,20 +380,11 @@ private fun ContactsScreen(navController: NavController) {
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(text = stringResource(R.string.contacts), fontWeight = FontWeight.Bold)
text = stringResource(R.string.contacts),
fontWeight = FontWeight.Bold
)
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -425,18 +396,13 @@ private fun ContactsScreen(navController: NavController) {
IconButton( IconButton(
onClick = { navController.navigateUp() } onClick = { navController.navigateUp() }
) { ) {
Icon( Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
} }
}, },
windowInsets = WindowInsets(0, 0, 0, 0),
actions = { actions = {
IconButton(onClick = { expanded = !expanded }) { IconButton(onClick = { expanded = !expanded }) {
Icon( Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu")
imageVector = Icons.Filled.Menu,
contentDescription = "Menu"
)
} }
CustomElements.DropdownMenu( CustomElements.DropdownMenu(
expanded = expanded, expanded = expanded,
@@ -498,7 +464,8 @@ private fun ContactsScreen(navController: NavController) {
) { ) {
Log.d(TAG, "Contacts permissions already granted") Log.d(TAG, "Contacts permissions already granted")
setContactsMode(mode) setContactsMode(mode)
} else { }
else {
if (shouldShowRequestPermissionRationale( if (shouldShowRequestPermissionRationale(
activity, Manifest.permission.READ_CONTACTS activity, Manifest.permission.READ_CONTACTS
) || ) ||
@@ -508,20 +475,20 @@ private fun ContactsScreen(navController: NavController) {
) { ) {
pendingMode = mode pendingMode = mode
showNoticeDialog.value = true showNoticeDialog.value = true
} else { }
else {
pendingMode = mode pendingMode = mode
requestPermissionsLauncher.launch(contactsPermissions) requestPermissionsLauncher.launch(contactsPermissions)
} }
} }
} }
showDialog.value = true showDialog.value = true
} else {
setContactsMode(mode)
} }
else
setContactsMode(mode)
} }
) )
}, }
windowInsets = WindowInsets(0, 0, 0, 0),
) )
} }
}, },
@@ -594,10 +561,7 @@ private fun BottomBar(
}, },
label = { Text(stringResource(R.string.search)) }, label = { Text(stringResource(R.string.search)) },
textStyle = TextStyle(fontSize = 18.sp), textStyle = TextStyle(fontSize = 18.sp),
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done)
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
)
) )
SmallFloatingActionButton( SmallFloatingActionButton(
onClick = { navController.navigate("contact//new") }, onClick = { navController.navigate("contact//new") },
@@ -641,18 +605,17 @@ private fun ContactsContent(
var lastSearchQuery by remember { mutableStateOf("") } var lastSearchQuery by remember { mutableStateOf("") }
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
if (searchQuery.isBlank() && lastSearchQuery.isNotBlank()) { if (searchQuery.isBlank() && lastSearchQuery.isNotBlank())
lazyListState.scrollToItem(0) lazyListState.scrollToItem(0)
}
lastSearchQuery = searchQuery lastSearchQuery = searchQuery
} }
val filteredContacts = remember(BaresipService.contacts, searchQuery) { val filteredContacts = remember(BaresipService.contacts, searchQuery) {
if (searchQuery.isBlank()) { if (searchQuery.isBlank())
BaresipService.contacts.map { contact -> BaresipService.contacts.map { contact ->
Pair(contact, buildAnnotatedString { append(contact.name()) }) Pair(contact, buildAnnotatedString { append(contact.name()) })
} }
} else { else {
val normalizedQuery = Utils.unaccent(searchQuery) val normalizedQuery = Utils.unaccent(searchQuery)
BaresipService.contacts BaresipService.contacts
.filter { contact -> .filter { contact ->
@@ -667,9 +630,8 @@ private fun ContactsContent(
LaunchedEffect(scrollToContact?.value, filteredContacts) { LaunchedEffect(scrollToContact?.value, filteredContacts) {
scrollToContact?.value?.let { name -> scrollToContact?.value?.let { name ->
val index = filteredContacts.indexOfFirst { it.first.name() == name } val index = filteredContacts.indexOfFirst { it.first.name() == name }
if (index != -1) { if (index != -1)
lazyListState.scrollToItem(index) lazyListState.scrollToItem(index)
}
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("scrollToContact") navController.currentBackStackEntry?.savedStateHandle?.remove<String>("scrollToContact")
} }
} }
@@ -687,13 +649,11 @@ private fun ContactsContent(
itemsIndexed( itemsIndexed(
filteredContacts, key = { index, (contact, _) -> "${contact.id()}_$index" } filteredContacts, key = { index, (contact, _) -> "${contact.id()}_$index" }
) { _, (contact, annotatedName) -> ) { _, (contact, annotatedName) ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
when (contact) { when (contact) {
is Contact.BaresipContact -> { is Contact.BaresipContact -> {
val avatarImage = contact.avatarImage val avatarImage = contact.avatarImage
if (avatarImage != null) if (avatarImage != null)
@@ -708,7 +668,6 @@ private fun ContactsContent(
else else
TextAvatar(contact.name(), contact.color) TextAvatar(contact.name(), contact.color)
} }
is Contact.AndroidContact -> { is Contact.AndroidContact -> {
val thumbNailUri = contact.thumbnailUri val thumbNailUri = contact.thumbnailUri
if (thumbNailUri != null) if (thumbNailUri != null)
@@ -724,45 +683,32 @@ private fun ContactsContent(
TextAvatar(contact.name(), contact.color) TextAvatar(contact.name(), contact.color)
} }
} }
when (contact) { when (contact) {
is Contact.BaresipContact -> { is Contact.BaresipContact -> {
Text(text = annotatedName, Text(
text = annotatedName,
fontSize = 20.sp, fontSize = 20.sp,
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal, fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 10.dp) .padding(start = 10.dp)
.combinedClickable( .combinedClickable(
onClick = { onClick = { navController.navigate("contact/${contact.name()}/old") },
navController.navigate("contact/${contact.name()}/old")
},
onLongClick = { onLongClick = {
title.value = confirmationText title.value = confirmationText
message.value = String.format( message.value = String.format(contactDeleteQuestion, contact.name())
contactDeleteQuestion,
contact.name()
)
firstButtonText.value = cancelText firstButtonText.value = cancelText
onFirstClicked.value = { } onFirstClicked.value = { }
lastButtonText.value = deleteText lastButtonText.value = deleteText
onLastClicked.value = { onLastClicked.value = {
val id = contact.id val id = contact.id
val avatarFile = File( val avatarFile = File(BaresipService.filesPath, "$id.png")
BaresipService.filesPath, if (avatarFile.exists())
"$id.png"
)
if (avatarFile.exists()) {
try { try {
avatarFile.delete() avatarFile.delete()
} catch (e: IOException) { } catch (e: IOException) {
Log.e( Log.e(TAG, "Could not delete file $id.png: ${e.message}")
TAG,
"Could not delete file $id.png: ${e.message}"
)
} }
}
Contact.removeBaresipContact(contact) Contact.removeBaresipContact(contact)
} }
showDialog.value = true showDialog.value = true
@@ -770,7 +716,6 @@ private fun ContactsContent(
) )
) )
} }
is Contact.AndroidContact -> { is Contact.AndroidContact -> {
Text(text = annotatedName, Text(text = annotatedName,
fontSize = 20.sp, fontSize = 20.sp,
@@ -779,15 +724,10 @@ private fun ContactsContent(
.weight(1f) .weight(1f)
.padding(start = 10.dp, top = 4.dp, bottom = 4.dp) .padding(start = 10.dp, top = 4.dp, bottom = 4.dp)
.combinedClickable( .combinedClickable(
onClick = { onClick = { navController.navigate("contact/${contact.name()}/old") },
navController.navigate("contact/${contact.name()}/old")
},
onLongClick = { onLongClick = {
title.value = confirmationText title.value = confirmationText
message.value = String.format( message.value = String.format(contactDeleteQuestion, contact.name())
contactDeleteQuestion,
contact.name()
)
firstButtonText.value = cancelText firstButtonText.value = cancelText
onFirstClicked.value = { } onFirstClicked.value = { }
lastButtonText.value = deleteText lastButtonText.value = deleteText
@@ -41,7 +41,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -50,7 +49,7 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -242,12 +241,10 @@ private fun MainScreen(
BaresipService.isMainVisible = true BaresipService.isMainVisible = true
viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone) viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone)
viewModel.updateCalls(Call.calls().toList()) viewModel.updateCalls(Call.calls().toList())
if (Call.inCall()) if (Call.inCall())
(ctx as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) (ctx as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else else
(ctx as? Activity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) (ctx as? Activity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
(Call.call("incoming") ?: Call.calls().lastOrNull())?.let { (Call.call("incoming") ?: Call.calls().lastOrNull())?.let {
spinToAor(viewModel, it.ua.account.aor, it) spinToAor(viewModel, it.ua.account.aor, it)
} ?: run { } ?: run {
@@ -453,7 +450,8 @@ private fun MainScreen(
showPasswordsDialog.value = true showPasswordsDialog.value = true
} }
) )
} else { }
else {
showPasswordsDialog.value = false showPasswordsDialog.value = false
showPasswordsDialog.value = true showPasswordsDialog.value = true
} }
@@ -471,27 +469,19 @@ private fun MainScreen(
Charsets.UTF_8 Charsets.UTF_8
).lines().toMutableList() ).lines().toMutableList()
showPasswordsDialog.value = true showPasswordsDialog.value = true
} else { }
else
// Baresip is started for the first time // Baresip is started for the first time
onRequestPermissions() onRequestPermissions()
}
} }
} }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding(),
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
viewModel = viewModel, viewModel = viewModel,
navController = navController, navController = navController,
@@ -541,11 +531,7 @@ private fun TopAppBar(
TopAppBar( TopAppBar(
title = { title = {
Text( Text(text = stringResource(R.string.baresip), fontSize = 22.sp, fontWeight = FontWeight.Bold)
text = stringResource(R.string.baresip),
fontSize = 22.sp,
fontWeight = FontWeight.Bold
)
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -736,10 +722,7 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
val buttonSize = 48.dp val buttonSize = 48.dp
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().navigationBarsPadding().padding(bottom = 16.dp),
.fillMaxWidth()
.navigationBarsPadding()
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -757,7 +740,8 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
intent.putExtra("uap", ua.uap) intent.putExtra("uap", ua.uap)
intent.putExtra("peer", acc.vmUri) intent.putExtra("peer", acc.vmUri)
handleIntent(ctx, viewModel, intent, "call") handleIntent(ctx, viewModel, intent, "call")
} else { }
else {
dialogTitle.value = ctx.getString(R.string.voicemail_messages) dialogTitle.value = ctx.getString(R.string.voicemail_messages)
dialogMessage.value = acc.vmMessages(ctx) dialogMessage.value = acc.vmMessages(ctx)
firstText.value = ctx.getString(R.string.cancel) firstText.value = ctx.getString(R.string.cancel)
@@ -774,23 +758,22 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
} }
} }
}, },
modifier = Modifier modifier = Modifier.weight(1f).size(buttonSize)
.weight(1f)
.size(buttonSize)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Voicemail, imageVector = Icons.Filled.Voicemail,
contentDescription = null, contentDescription = null,
Modifier.size(buttonSize), Modifier.size(buttonSize),
tint = if (hasNewVoicemail) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary tint = if (hasNewVoicemail)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.secondary
) )
} }
IconButton( IconButton(
onClick = { navController.navigate("contacts") }, onClick = { navController.navigate("contacts") },
modifier = Modifier modifier = Modifier.weight(1f).size(buttonSize)
.weight(1f)
.size(buttonSize)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Person, imageVector = Icons.Filled.Person,
@@ -803,13 +786,11 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
IconButton( IconButton(
enabled = aor.isNotEmpty(), enabled = aor.isNotEmpty(),
onClick = { onClick = {
if (isMobile) { if (isMobile && !Utils.isDefaultSmsApp(ctx)) {
if (!Utils.isDefaultSmsApp(ctx)) { alertTitle.value = ctx.getString(R.string.notice)
alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = ctx.getString(R.string.enable_default_messaging)
alertMessage.value = ctx.getString(R.string.enable_default_messaging) showAlert.value = true
showAlert.value = true return@IconButton
return@IconButton
}
} }
navController.navigate("chats/$aor") navController.navigate("chats/$aor")
}, },
@@ -828,12 +809,8 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
IconButton( IconButton(
enabled = aor.isNotEmpty(), enabled = aor.isNotEmpty(),
onClick = { onClick = { navController.navigate("calls/$aor") },
navController.navigate("calls/$aor") modifier = Modifier.weight(1f).size(buttonSize)
},
modifier = Modifier
.weight(1f)
.size(buttonSize)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.History, imageVector = Icons.Filled.History,
@@ -845,9 +822,7 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
IconButton( IconButton(
onClick = { viewModel.toggleDialpadVisibility() }, onClick = { viewModel.toggleDialpadVisibility() },
modifier = Modifier modifier = Modifier.weight(1f).size(buttonSize),
.weight(1f)
.size(buttonSize),
enabled = dialpadButtonEnabled.value enabled = dialpadButtonEnabled.value
) { ) {
Icon( Icon(
@@ -985,7 +960,8 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
} }
} else if (offset > swipeThreshold) { }
else if (offset > swipeThreshold) {
if (uas.value.isNotEmpty()) { if (uas.value.isNotEmpty()) {
val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value) val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
val newPos = when (curPos) { val newPos = when (curPos) {
@@ -1046,26 +1022,18 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
val selected = viewModel.selectedAor.value val selected = viewModel.selectedAor.value
if (uas.value.isEmpty()) { if (uas.value.isEmpty()) {
if (selected != "") viewModel.updateSelectedAor("") if (selected != "") viewModel.updateSelectedAor("")
} else {
if (selected == "" || UserAgent.ofAor(selected) == null) {
viewModel.updateSelectedAor(uas.value.first().account.aor)
}
} }
else if (selected == "" || UserAgent.ofAor(selected) == null)
viewModel.updateSelectedAor(uas.value.first().account.aor)
val ua = UserAgent.ofAor(viewModel.selectedAor.value) val ua = UserAgent.ofAor(viewModel.selectedAor.value)
if (ua != null) { if (ua != null)
showCall(ctx, viewModel, ua, viewModel.focusedCall.value) showCall(ctx, viewModel, ua, viewModel.focusedCall.value)
}
} }
if (selected == "") { if (selected == "") {
OutlinedButton( OutlinedButton(
onClick = { onClick = { navController.navigate("accounts") },
navController.navigate("accounts") modifier = Modifier.padding(horizontal = 4.dp).height(50.dp).fillMaxWidth(),
},
modifier = Modifier
.padding(horizontal = 4.dp)
.height(50.dp)
.fillMaxWidth(),
colors = ButtonColors( colors = ButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant, containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant, contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -1080,18 +1048,14 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
} }
else else
OutlinedButton( OutlinedButton(
onClick = { onClick = { expanded = !expanded },
expanded = !expanded
},
enabled = true, enabled = true,
modifier = Modifier modifier = Modifier
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.height(50.dp) .height(50.dp)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onPress = { onPress = { expanded = true },
expanded = true
},
onLongPress = { onLongPress = {
val ua = UserAgent.ofAor(selected) val ua = UserAgent.ofAor(selected)
if (ua != null) { if (ua != null) {
@@ -1100,11 +1064,9 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
if (Api.account_regint(acc.accp) > 0) { if (Api.account_regint(acc.accp) > 0) {
Api.account_set_regint(acc.accp, 0) Api.account_set_regint(acc.accp, 0)
Api.ua_unregister(ua.uap) Api.ua_unregister(ua.uap)
} else { }
Api.account_set_regint( else {
acc.accp, Api.account_set_regint(acc.accp, acc.configuredRegInt)
acc.configuredRegInt
)
Api.ua_register(ua.uap) Api.ua_register(ua.uap)
} }
acc.regint = Api.account_regint(acc.accp) acc.regint = Api.account_regint(acc.accp)
@@ -1153,7 +1115,8 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
if (Api.account_regint(acc.accp) > 0) { if (Api.account_regint(acc.accp) > 0) {
Api.account_set_regint(acc.accp, 0) Api.account_set_regint(acc.accp, 0)
Api.ua_unregister(ua.uap) Api.ua_unregister(ua.uap)
} else { }
else {
Api.account_set_regint( Api.account_set_regint(
acc.accp, acc.accp,
acc.configuredRegInt acc.configuredRegInt
@@ -1186,11 +1149,9 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
expanded = false expanded = false
spinToAor(viewModel, acc.aor) spinToAor(viewModel, acc.aor)
}, },
text = { Text( text = {
text = acc.text(), Text(text = acc.text(), fontSize = 17.sp, fontWeight = FontWeight.Bold)
fontSize = 17.sp, },
fontWeight = FontWeight.Bold
) },
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = ImageVector.vectorResource(uasStatus.value[acc.aor]!!), imageVector = ImageVector.vectorResource(uasStatus.value[acc.aor]!!),
@@ -1199,9 +1160,8 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
) )
} }
) )
if (index < uas.value.size - 1) { if (index < uas.value.size - 1)
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
} }
} }
} }
@@ -1224,21 +1184,16 @@ private fun CallUriRow(
val isDialpadVisible by viewModel.isDialpadVisible.collectAsState() val isDialpadVisible by viewModel.isDialpadVisible.collectAsState()
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 8.dp),
.fillMaxWidth()
.padding(top = 4.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField( OutlinedTextField(
value = if (isDialer) dialerState.callUri.value else call!!.callUri.value, value = if (isDialer) dialerState.callUri.value else call!!.callUri.value,
readOnly = if (isDialer) !dialerState.callUriEnabled.value else !call!!.callUriEnabled.value, readOnly = if (isDialer) !dialerState.callUriEnabled.value else !call!!.callUriEnabled.value,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = {
if (isDialer) { if (isDialer)
if (it != dialerState.callUri.value) { if (it != dialerState.callUri.value) {
dialerState.callUri.value = it dialerState.callUri.value = it
dialerState.redialUri = "" dialerState.redialUri = ""
@@ -1258,7 +1213,6 @@ private fun CallUriRow(
} }
dialerState.showSuggestions.value = it.length > 1 dialerState.showSuggestions.value = it.length > 1
} }
}
}, },
trailingIcon = { trailingIcon = {
if (isDialer && dialerState.callUriEnabled.value && dialerState.callUri.value.isNotEmpty()) if (isDialer && dialerState.callUriEnabled.value && dialerState.callUri.value.isNotEmpty())
@@ -1322,10 +1276,7 @@ private fun CallUriRow(
.fillMaxWidth() .fillMaxWidth()
.padding(start = 4.dp, end = 4.dp, top = 2.dp, bottom = 2.dp), .padding(start = 4.dp, end = 4.dp, top = 2.dp, bottom = 2.dp),
label = { label = {
Text( Text(text = call.callUriLabel2.value, fontSize = 18.sp)
text = call.callUriLabel2.value,
fontSize = 18.sp
)
}, },
textStyle = TextStyle(fontSize = 18.sp) textStyle = TextStyle(fontSize = 18.sp)
) )
@@ -1343,9 +1294,7 @@ private fun CallUriRow(
) { ) {
if (isDialer && dialerState.showSuggestions.value && filteredSuggestions.isNotEmpty()) { if (isDialer && dialerState.showSuggestions.value && filteredSuggestions.isNotEmpty()) {
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxWidth().heightIn(max = 150.dp)
.fillMaxWidth()
.heightIn(max = 150.dp)
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
@@ -1403,13 +1352,11 @@ private fun CallUriRow(
alertMessage.value = ctx.getString(R.string.call_not_secure) alertMessage.value = ctx.getString(R.string.call_not_secure)
showAlert.value = true showAlert.value = true
} }
R.color.colorTrafficYellow -> { R.color.colorTrafficYellow -> {
alertTitle.value = ctx.getString(R.string.alert) alertTitle.value = ctx.getString(R.string.alert)
alertMessage.value = ctx.getString(R.string.peer_not_verified) alertMessage.value = ctx.getString(R.string.peer_not_verified)
showAlert.value = true showAlert.value = true
} }
R.color.colorTrafficGreen -> { R.color.colorTrafficGreen -> {
dialogTitle.value = ctx.getString(R.string.info) dialogTitle.value = ctx.getString(R.string.info)
dialogMessage.value = ctx.getString(R.string.call_is_secure) dialogMessage.value = ctx.getString(R.string.call_is_secure)
@@ -1419,10 +1366,7 @@ private fun CallUriRow(
lastText.value = ctx.getString(R.string.unverify) lastText.value = ctx.getString(R.string.unverify)
onLastClicked.value = { onLastClicked.value = {
if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0) if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0)
Log.e( Log.e(TAG, "Command 'zrtp_unverify ${call.zid}' failed")
TAG,
"Command 'zrtp_unverify ${call.zid}' failed"
)
else else
call.securityIconTint.value = R.color.colorTrafficYellow call.securityIconTint.value = R.color.colorTrafficYellow
} }
@@ -1446,10 +1390,7 @@ private fun CallUriRow(
} }
@Composable @Composable
private fun CallTimer( private fun CallTimer(initialDurationSeconds: Long, modifier: Modifier = Modifier) {
initialDurationSeconds: Long,
modifier: Modifier = Modifier
) {
val startTime = remember(initialDurationSeconds) { val startTime = remember(initialDurationSeconds) {
SystemClock.elapsedRealtime() - (initialDurationSeconds * 1000L) SystemClock.elapsedRealtime() - (initialDurationSeconds * 1000L)
} }
@@ -1489,11 +1430,12 @@ private fun CallRow(
Row( modifier = Modifier Row( modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = if (isDialer || call?.showCancelButton?.value == true || horizontalArrangement =
call?.showAnswerRejectButtons?.value == true) if (isDialer || call?.showCancelButton?.value == true ||
Arrangement.Center call?.showAnswerRejectButtons?.value == true)
else Arrangement.Center
Arrangement.SpaceBetween else
Arrangement.SpaceBetween
) { ) {
if (isDialer) { if (isDialer) {
if (dialerState.showCallButton.value) if (dialerState.showCallButton.value)
@@ -1686,14 +1628,14 @@ private fun CallRow(
transferUri = it transferUri = it
if (it.length > 1) { if (it.length > 1) {
val normalizedInput = Utils.unaccent(it) val normalizedInput = Utils.unaccent(it)
filteredSuggestions = filteredSuggestions = suggestions
suggestions.filter { suggestion -> .filter { suggestion ->
Utils.unaccent(suggestion) Utils.unaccent(suggestion)
.contains(normalizedInput, ignoreCase = true) .contains(normalizedInput, ignoreCase = true)
} }
.map { suggestion -> .map { suggestion ->
Utils.buildAnnotatedStringWithHighlight(suggestion, it) Utils.buildAnnotatedStringWithHighlight(suggestion, it)
} }
} }
call.showSuggestions.value = transferUri.length > 1 call.showSuggestions.value = transferUri.length > 1
} }
@@ -1714,12 +1656,7 @@ private fun CallRow(
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp)
start = 4.dp,
end = 4.dp,
top = 12.dp,
bottom = 2.dp
)
.focusRequester(focusRequester), .focusRequester(focusRequester),
label = { Text(stringResource(R.string.transfer_destination)) }, label = { Text(stringResource(R.string.transfer_destination)) },
textStyle = TextStyle(fontSize = 18.sp), textStyle = TextStyle(fontSize = 18.sp),
@@ -1740,16 +1677,11 @@ private fun CallRow(
.animateContentSize() .animateContentSize()
) { ) {
if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) { if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) {
Box(modifier = Modifier Box(modifier = Modifier.fillMaxWidth().heightIn(max = 150.dp)) {
.fillMaxWidth()
.heightIn(max = 150.dp)) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScrollbar( .verticalScrollbar(state = lazyListState, width = 6.dp),
state = lazyListState,
width = 6.dp,
),
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
state = lazyListState, state = lazyListState,
) { ) {
@@ -1761,10 +1693,8 @@ private fun CallRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
transferUri = transferUri = suggestion.toString()
suggestion.toString() call.showSuggestions.value = false
call.showSuggestions.value =
false
} }
.padding(12.dp) .padding(12.dp)
) { ) {
@@ -1793,9 +1723,7 @@ private fun CallRow(
) )
Switch( Switch(
checked = blindChecked.value, checked = blindChecked.value,
onCheckedChange = { onCheckedChange = { blindChecked.value = true }
blindChecked.value = true
}
) )
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@@ -1807,9 +1735,7 @@ private fun CallRow(
) )
Switch( Switch(
checked = !blindChecked.value, checked = !blindChecked.value,
onCheckedChange = { onCheckedChange = { blindChecked.value = false }
blindChecked.value = false
}
) )
} }
} }
@@ -1902,14 +1828,9 @@ private fun CallRow(
} }
call.dtmfText.value = newText call.dtmfText.value = newText
}, },
modifier = Modifier modifier = Modifier.width(80.dp).focusRequester(focusRequester),
.width(80.dp)
.focusRequester(focusRequester),
enabled = call.dtmfEnabled.value, enabled = call.dtmfEnabled.value,
textStyle = TextStyle( textStyle = TextStyle(fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface),
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
singleLine = true, singleLine = true,
@@ -1923,17 +1844,9 @@ private fun CallRow(
enabled = call.dtmfEnabled.value, enabled = call.dtmfEnabled.value,
interactionSource = interactionSource, interactionSource = interactionSource,
label = { label = {
Text( Text(stringResource(R.string.dtmf), style = TextStyle(fontSize = 12.sp))
stringResource(R.string.dtmf),
style = TextStyle(fontSize = 12.sp)
)
}, },
contentPadding = PaddingValues( contentPadding = PaddingValues(start = 4.dp, end = 4.dp, top = 8.dp, bottom = 8.dp),
start = 4.dp,
end = 4.dp,
top = 8.dp,
bottom = 8.dp
),
colors = OutlinedTextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
@@ -1979,7 +1892,8 @@ private fun CallRow(
"${ctx.getString(R.string.lost)}: ${parts[3]}\n" + "${ctx.getString(R.string.lost)}: ${parts[3]}\n" +
String.format(ctx.getString(R.string.jitter), parts[4]) String.format(ctx.getString(R.string.jitter), parts[4])
showAlert.value = true showAlert.value = true
} else { }
else {
alertTitle.value = ctx.getString(R.string.call_info) alertTitle.value = ctx.getString(R.string.call_info)
alertMessage.value = ctx.getString(R.string.call_info_not_available) alertMessage.value = ctx.getString(R.string.call_info_not_available)
showAlert.value = true showAlert.value = true
@@ -1995,12 +1909,9 @@ private fun CallRow(
} }
if (call.showAnswerRejectButtons.value) { if (call.showAnswerRejectButtons.value) {
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = { answer(ctx, call) },
answer(ctx, call)
},
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Call, imageVector = Icons.Filled.Call,
@@ -2009,14 +1920,10 @@ private fun CallRow(
contentDescription = null, contentDescription = null,
) )
} }
Spacer(Modifier.width(48.dp)) Spacer(Modifier.width(48.dp))
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = { reject(call) },
reject(call)
},
) { ) {
Icon( Icon(
imageVector = Icons.Filled.CallEnd, imageVector = Icons.Filled.CallEnd,
@@ -2039,10 +1946,7 @@ private fun OnHoldNotice() {
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
shape = RoundedCornerShape(20) shape = RoundedCornerShape(20)
) { ) {
Text( Text(text = stringResource(R.string.call_is_on_hold), fontSize = 18.sp)
text = stringResource(R.string.call_is_on_hold),
fontSize = 18.sp
)
} }
} }
} }
@@ -2252,8 +2156,7 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText:
} }
private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCall: Call? = null) { private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCall: Call? = null) {
if (ua == null) if (ua == null) return
return
val call = showCall ?: ua.currentCall() val call = showCall ?: ua.currentCall()
val aor = ua.account.aor val aor = ua.account.aor
val callp = call?.callp ?: 0L val callp = call?.callp ?: 0L
@@ -2278,7 +2181,8 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
BaresipService.isMicMuted = false BaresipService.isMicMuted = false
viewModel.updateMicIcon(Icons.Filled.Mic) viewModel.updateMicIcon(Icons.Filled.Mic)
} }
} else { }
else {
viewModel.dialerState.callUri.value = "" viewModel.dialerState.callUri.value = ""
pullToRefreshEnabled.value = false pullToRefreshEnabled.value = false
call.callUriEnabled.value = false call.callUriEnabled.value = false
@@ -2323,9 +2227,8 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
call.callUriLabel2.value = ctx.getString(R.string.diverted_by_dots) call.callUriLabel2.value = ctx.getString(R.string.diverted_by_dots)
call.callUri2.value = Utils.friendlyUri(ctx, uri, ua.account) call.callUri2.value = Utils.friendlyUri(ctx, uri, ua.account)
} }
else { else
call.callUri2.value = "" call.callUri2.value = ""
}
call.showCallButton.value = false call.showCallButton.value = false
call.showCancelButton.value = false call.showCancelButton.value = false
call.showHangupButton.value = false call.showHangupButton.value = false
@@ -2388,13 +2291,10 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
Log.d(TAG, "Handling service event 'started' with URI '$uriString'") Log.d(TAG, "Handling service event 'started' with URI '$uriString'")
if (uriString != "") if (uriString != "")
callAction(ctx, viewModel, uriString.toUri(), "dial") callAction(ctx, viewModel, uriString.toUri(), "dial")
else { else if (viewModel.selectedAor.value == "" && uas.value.isNotEmpty())
if (viewModel.selectedAor.value == "" && uas.value.isNotEmpty()) viewModel.updateSelectedAor(uas.value.first().account.aor)
viewModel.updateSelectedAor(uas.value.first().account.aor) if (Preferences(ctx).displayTheme != AppCompatDelegate.getDefaultNightMode())
}
if (Preferences(ctx).displayTheme != AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.setDefaultNightMode(Preferences(ctx).displayTheme) AppCompatDelegate.setDefaultNightMode(Preferences(ctx).displayTheme)
}
handleNextEvent() handleNextEvent()
return return
} }
@@ -2430,8 +2330,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
val aor = ua.account.aor val aor = ua.account.aor
when (ev[0]) { when (ev[0]) {
"call rejected" -> { "call rejected" -> {}
}
"call outgoing" -> { "call outgoing" -> {
val callp = params[1] as Long val callp = params[1] as Long
if (!BaresipService.isMainVisible) if (!BaresipService.isMainVisible)
@@ -2467,9 +2366,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
secondText.value = ctx.getString(R.string.no) secondText.value = ctx.getString(R.string.no)
onSecondClicked.value = { } onSecondClicked.value = { }
lastText.value = ctx.getString(R.string.yes) lastText.value = ctx.getString(R.string.yes)
onLastClicked.value = { onLastClicked.value = { redirect(ctx, viewModel, ua, redirectUri) }
redirect(ctx, viewModel, ua, redirectUri)
}
showDialog.value = true showDialog.value = true
} }
} }
@@ -2483,8 +2380,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
call.dtmfText.value = "" call.dtmfText.value = ""
} }
} }
"call update" -> { "call update" -> {}
}
"call verify" -> { "call verify" -> {
val callp = params[1] as Long val callp = params[1] as Long
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
@@ -2508,9 +2404,9 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
call.security = if (Api.cmd_exec("zrtp_verify ${ev[2]}") != 0) { call.security = if (Api.cmd_exec("zrtp_verify ${ev[2]}") != 0) {
Log.e(TAG, "Command 'zrtp_verify ${ev[2]}' failed") Log.e(TAG, "Command 'zrtp_verify ${ev[2]}' failed")
R.color.colorTrafficYellow R.color.colorTrafficYellow
} else {
R.color.colorTrafficGreen
} }
else
R.color.colorTrafficGreen
call.zid = ev[2] call.zid = ev[2]
if (aor == viewModel.selectedAor.value) if (aor == viewModel.selectedAor.value)
call.securityIconTint.value = call.security call.securityIconTint.value = call.security
@@ -2524,9 +2420,8 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
handleNextEvent("Call $callp that is verified is not found") handleNextEvent("Call $callp that is verified is not found")
return return
} }
if (aor == viewModel.selectedAor.value) { if (aor == viewModel.selectedAor.value)
call.securityIconTint.value = call.security call.securityIconTint.value = call.security
}
} }
"call transfer", "transfer show" -> { "call transfer", "transfer show" -> {
if (!BaresipService.isMainVisible) if (!BaresipService.isMainVisible)
@@ -2594,14 +2489,13 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
} }
"mwi notify" -> { "mwi notify" -> {
val lines = ev[1].split("\n") val lines = ev[1].split("\n")
for (line in lines) { for (line in lines)
if (line.startsWith("Voice-Message:")) { if (line.startsWith("Voice-Message:")) {
val counts = (line.split(" ")[1]).split("/") val counts = (line.split(" ")[1]).split("/")
acc.vmNew = counts[0].toInt() acc.vmNew = counts[0].toInt()
acc.vmOld = counts[1].toInt() acc.vmOld = counts[1].toInt()
break break
} }
}
} }
else -> Log.e(TAG, "Unknown event '${ev[0]}'") else -> Log.e(TAG, "Unknown event '${ev[0]}'")
} }
@@ -2648,10 +2542,8 @@ fun handleIntent(ctx: Context, viewModel: ViewModel, intent: Intent, action: Str
if (ev[0] == "call answer") if (ev[0] == "call answer")
answer(ctx, call) answer(ctx, call)
else else
BaresipService.postServiceEvent(ServiceEvent( BaresipService.postServiceEvent(
"call incoming", ServiceEvent("call incoming", arrayListOf(call.ua.uap, callp), System.nanoTime())
arrayListOf(call.ua.uap, callp),
System.nanoTime())
) )
} }
"call missed" -> { "call missed" -> {
@@ -2676,10 +2568,8 @@ fun handleIntent(ctx: Context, viewModel: ViewModel, intent: Intent, action: Str
ev[1] ev[1]
else else
intent.getStringExtra("uri")!! intent.getStringExtra("uri")!!
BaresipService.postServiceEvent(ServiceEvent( BaresipService.postServiceEvent(
ev[0] + "," + uri, ServiceEvent(ev[0] + "," + uri, arrayListOf(call.ua.uap, callp), System.nanoTime())
arrayListOf(call.ua.uap, callp),
System.nanoTime())
) )
} }
"message", "message show", "message reply" -> { "message", "message show", "message reply" -> {
@@ -2690,10 +2580,8 @@ fun handleIntent(ctx: Context, viewModel: ViewModel, intent: Intent, action: Str
return return
} }
spinToAor(viewModel, ua.account.aor) spinToAor(viewModel, ua.account.aor)
BaresipService.postServiceEvent(ServiceEvent( BaresipService.postServiceEvent(
ev[0], ServiceEvent(ev[0], arrayListOf(uap, intent.getStringExtra("peer")!!), System.nanoTime())
arrayListOf(uap, intent.getStringExtra("peer")!!),
System.nanoTime())
) )
} }
} }
@@ -2710,8 +2598,7 @@ fun handleDialog(ctx: Context, title: String, message: String, action: () -> Uni
} }
fun callAction(ctx: Context, viewModel: ViewModel, uri: Uri?, action: String) { fun callAction(ctx: Context, viewModel: ViewModel, uri: Uri?, action: String) {
if (Call.inCall() || uas.value.isEmpty()) if (Call.inCall() || uas.value.isEmpty()) return
return
Log.d(TAG, "Action $action to $uri") Log.d(TAG, "Action $action to $uri")
if (uri != null) { if (uri != null) {
var uriStr: String var uriStr: String
@@ -2772,11 +2659,13 @@ private fun acceptTransfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, ca
if (ua.account.aor != viewModel.selectedAor.value) if (ua.account.aor != viewModel.selectedAor.value)
spinToAor(viewModel, ua.account.aor) spinToAor(viewModel, ua.account.aor)
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} else { }
else {
Log.w(TAG, "call_connect $newCallp failed") Log.w(TAG, "call_connect $newCallp failed")
call.notifySipfrag(500, "Call Error") call.notifySipfrag(500, "Call Error")
} }
} else { }
else {
Log.w(TAG, "callAlloc for ua ${ua.uap} call ${call.callp} transfer failed") Log.w(TAG, "callAlloc for ua ${ua.uap} call ${call.callp} transfer failed")
call.notifySipfrag(500, "Call Error") call.notifySipfrag(500, "Call Error")
} }
@@ -2794,8 +2683,10 @@ private fun backup(ctx: Context, password: String) {
if (!Utils.zip(files, zipFile)) { if (!Utils.zip(files, zipFile)) {
Log.w(TAG, "Failed to write zip file '$zipFile'") Log.w(TAG, "Failed to write zip file '$zipFile'")
alertTitle.value = ctx.getString(R.string.error) alertTitle.value = ctx.getString(R.string.error)
alertMessage.value = String.format(ctx.getString(R.string.backup_failed), alertMessage.value = String.format(
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) ctx.getString(R.string.backup_failed),
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)
)
showAlert.value = true showAlert.value = true
downloadsOutputUri = null downloadsOutputUri = null
return return
@@ -2804,23 +2695,29 @@ private fun backup(ctx: Context, password: String) {
if (content == null) { if (content == null) {
Log.w(TAG, "Failed to read zip file '$zipFile'") Log.w(TAG, "Failed to read zip file '$zipFile'")
alertTitle.value = ctx.getString(R.string.error) alertTitle.value = ctx.getString(R.string.error)
alertMessage.value = String.format(ctx.getString(R.string.backup_failed), alertMessage.value = String.format(
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) ctx.getString(R.string.backup_failed),
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)
)
showAlert.value = true showAlert.value = true
downloadsOutputUri = null downloadsOutputUri = null
return return
} }
if (!Utils.encryptToUri(ctx, downloadsOutputUri!!, content, password)) { if (!Utils.encryptToUri(ctx, downloadsOutputUri!!, content, password)) {
alertTitle.value = ctx.getString(R.string.error) alertTitle.value = ctx.getString(R.string.error)
alertMessage.value = String.format(ctx.getString(R.string.backup_failed), alertMessage.value = String.format(
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) ctx.getString(R.string.backup_failed),
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)
)
showAlert.value = true showAlert.value = true
downloadsOutputUri = null downloadsOutputUri = null
return return
} }
alertTitle.value = ctx.getString(R.string.info) alertTitle.value = ctx.getString(R.string.info)
alertMessage.value = String.format(ctx.getString(R.string.backed_up), alertMessage.value = String.format(
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) ctx.getString(R.string.backed_up),
Utils.fileNameOfUri(ctx, downloadsOutputUri!!)
)
showAlert.value = true showAlert.value = true
Utils.deleteFile(File(zipFilePath)) Utils.deleteFile(File(zipFilePath))
downloadsInputUri = null downloadsInputUri = null
@@ -2832,8 +2729,10 @@ private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
val zipData = Utils.decryptFromUri(ctx, downloadsInputUri!!, password) val zipData = Utils.decryptFromUri(ctx, downloadsInputUri!!, password)
if (zipData == null) { if (zipData == null) {
alertTitle.value = ctx.getString(R.string.error) alertTitle.value = ctx.getString(R.string.error)
alertMessage.value = String.format(ctx.getString(R.string.restore_failed), alertMessage.value = String.format(
Utils.fileNameOfUri(ctx, downloadsInputUri!!)) ctx.getString(R.string.restore_failed),
Utils.fileNameOfUri(ctx, downloadsInputUri!!)
)
showAlert.value = true showAlert.value = true
downloadsInputUri = null downloadsInputUri = null
return return
@@ -2841,8 +2740,10 @@ private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
if (!Utils.putFileContents(zipFilePath, zipData)) { if (!Utils.putFileContents(zipFilePath, zipData)) {
Log.w(TAG, "Failed to write zip file '$zipFile'") Log.w(TAG, "Failed to write zip file '$zipFile'")
alertTitle.value = ctx.getString(R.string.error) alertTitle.value = ctx.getString(R.string.error)
alertMessage.value = String.format(ctx.getString(R.string.restore_failed), alertMessage.value = String.format(
Utils.fileNameOfUri(ctx, downloadsInputUri!!)) ctx.getString(R.string.restore_failed),
Utils.fileNameOfUri(ctx, downloadsInputUri!!)
)
showAlert.value = true showAlert.value = true
downloadsInputUri = null downloadsInputUri = null
return return
@@ -2871,9 +2772,7 @@ private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
dialogTitle.value = ctx.getString(R.string.info) dialogTitle.value = ctx.getString(R.string.info)
dialogMessage.value = ctx.getString(R.string.restored) dialogMessage.value = ctx.getString(R.string.restored)
firstText.value = ctx.getString(R.string.cancel) firstText.value = ctx.getString(R.string.cancel)
onFirstClicked.value = { onFirstClicked.value = { showDialog.value = false }
showDialog.value = false
}
secondText.value = "" secondText.value = ""
lastText.value = ctx.getString(R.string.restart) lastText.value = ctx.getString(R.string.restart)
onLastClicked.value = { onLastClicked.value = {
@@ -25,14 +25,15 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -155,25 +156,14 @@ private fun SettingsScreen(
} }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding().navigationBarsPadding(),
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
modifier = Modifier Spacer(Modifier.statusBarsPadding())
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
)
) {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(text = stringResource(R.string.configuration), fontWeight = FontWeight.Bold)
text = stringResource(R.string.configuration),
fontWeight = FontWeight.Bold
)
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -185,17 +175,14 @@ private fun SettingsScreen(
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null, contentDescription = "Back",
) )
} }
}, },
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
actions = { actions = {
IconButton(onClick = checkOnClick) { IconButton(onClick = checkOnClick) {
Icon( Icon(imageVector = Icons.Filled.Check, contentDescription = "Check")
imageVector = Icons.Filled.Check,
contentDescription = "Check"
)
} }
}, },
) )
@@ -209,13 +196,9 @@ private fun SettingsScreen(
title = stringResource(R.string.restart_request), title = stringResource(R.string.restart_request),
message = stringResource(R.string.config_restart), message = stringResource(R.string.config_restart),
firstButtonText = stringResource(R.string.cancel), firstButtonText = stringResource(R.string.cancel),
onFirstClicked = { onFirstClicked = { navController.navigateUp() },
navController.navigateUp()
},
lastButtonText = stringResource(R.string.restart), lastButtonText = stringResource(R.string.restart),
onLastClicked = { onLastClicked = { onRestartApp() },
onRestartApp()
},
) )
} }
@@ -304,9 +287,7 @@ private fun SettingsContent(
val startAutomaticallyHelp = stringResource(R.string.start_automatically_help) val startAutomaticallyHelp = stringResource(R.string.start_automatically_help)
val appearOnTopPermissionMessage = stringResource(R.string.appear_on_top_permission) val appearOnTopPermissionMessage = stringResource(R.string.appear_on_top_permission)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -355,9 +336,7 @@ private fun SettingsContent(
val addressFamilyTitle = stringResource(R.string.address_family) val addressFamilyTitle = stringResource(R.string.address_family)
val addressFamilyHelp = stringResource(R.string.address_family_help) val addressFamilyHelp = stringResource(R.string.address_family_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -372,9 +351,7 @@ private fun SettingsContent(
}, },
fontSize = 18.sp fontSize = 18.sp
) )
val isDropDownExpanded = remember { val isDropDownExpanded = remember { mutableStateOf(false) }
mutableStateOf(false)
}
val familyNames = listOf("--", "IPv4", "IPv6") val familyNames = listOf("--", "IPv4", "IPv6")
val familyValues = listOf("", "ipv4", "ipv6") val familyValues = listOf("", "ipv4", "ipv6")
val itemPosition = remember { mutableIntStateOf(familyValues.indexOf(addressFamily)) } val itemPosition = remember { mutableIntStateOf(familyValues.indexOf(addressFamily)) }
@@ -382,9 +359,7 @@ private fun SettingsContent(
Row( Row(
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { modifier = Modifier.clickable { isDropDownExpanded.value = true }
isDropDownExpanded.value = true
}
) { ) {
Text(text = familyNames[itemPosition.intValue]) Text(text = familyNames[itemPosition.intValue])
Icon( Icon(
@@ -396,9 +371,8 @@ private fun SettingsContent(
} }
DropdownMenu( DropdownMenu(
expanded = isDropDownExpanded.value, expanded = isDropDownExpanded.value,
onDismissRequest = { onDismissRequest = { isDropDownExpanded.value = false }
isDropDownExpanded.value = false ) {
}) {
familyNames.forEachIndexed { index, family -> familyNames.forEachIndexed { index, family ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = family) }, text = { Text(text = family) },
@@ -420,9 +394,7 @@ private fun SettingsContent(
val listenAddressTitle = stringResource(R.string.listen_address) val listenAddressTitle = stringResource(R.string.listen_address)
val listenAddressHelp = stringResource(R.string.listen_address_help) val listenAddressHelp = stringResource(R.string.listen_address_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
) { ) {
@@ -430,9 +402,7 @@ private fun SettingsContent(
OutlinedTextField( OutlinedTextField(
value = listenAddress, value = listenAddress,
placeholder = { Text(stringResource(R.string._0_0_0_0_5060)) }, placeholder = { Text(stringResource(R.string._0_0_0_0_5060)) },
onValueChange = { onValueChange = { viewModel.listenAddress.value = it },
viewModel.listenAddress.value = it
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
@@ -453,18 +423,14 @@ private fun SettingsContent(
val transportProtocolsTitle = stringResource(R.string.transport_protocols) val transportProtocolsTitle = stringResource(R.string.transport_protocols)
val transportProtocolsHelp = stringResource(R.string.transport_protocols_help) val transportProtocolsHelp = stringResource(R.string.transport_protocols_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
val transportProtocols by viewModel.transportProtocols.collectAsState() val transportProtocols by viewModel.transportProtocols.collectAsState()
OutlinedTextField( OutlinedTextField(
value = transportProtocols, value = transportProtocols,
onValueChange = { onValueChange = { viewModel.transportProtocols.value = it },
viewModel.transportProtocols.value = it
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
@@ -484,18 +450,14 @@ private fun SettingsContent(
val dnsServersTitle = stringResource(R.string.dns_servers) val dnsServersTitle = stringResource(R.string.dns_servers)
val dnsServersHelp = stringResource(R.string.dns_servers_help) val dnsServersHelp = stringResource(R.string.dns_servers_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
val dnsServers by viewModel.dnsServers.collectAsState() val dnsServers by viewModel.dnsServers.collectAsState()
OutlinedTextField( OutlinedTextField(
value = dnsServers, value = dnsServers,
onValueChange = { onValueChange = { viewModel.dnsServers.value = it },
viewModel.dnsServers.value = it
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
@@ -555,9 +517,7 @@ private fun SettingsContent(
val showAlertDialog = remember { mutableStateOf(false) } val showAlertDialog = remember { mutableStateOf(false) }
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -635,9 +595,7 @@ private fun SettingsContent(
val verifyServerTitle = stringResource(R.string.verify_server) val verifyServerTitle = stringResource(R.string.verify_server)
val verifyServerHelp = stringResource(R.string.verify_server_help) val verifyServerHelp = stringResource(R.string.verify_server_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -697,9 +655,7 @@ private fun SettingsContent(
} }
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -719,7 +675,7 @@ private fun SettingsContent(
checked = caFile, checked = caFile,
onCheckedChange = { onCheckedChange = {
viewModel.caFile.value = it viewModel.caFile.value = it
if (it) { if (it)
if (VERSION.SDK_INT < 29) { if (VERSION.SDK_INT < 29) {
viewModel.caFile.value = false viewModel.caFile.value = false
val permission = Manifest.permission.READ_EXTERNAL_STORAGE val permission = Manifest.permission.READ_EXTERNAL_STORAGE
@@ -754,7 +710,6 @@ private fun SettingsContent(
} }
else else
Utils.selectInputFile(caCertsRequest) Utils.selectInputFile(caCertsRequest)
}
else { else {
Utils.deleteFile(File(BaresipService.filesPath + "/ca_certs.crt")) Utils.deleteFile(File(BaresipService.filesPath + "/ca_certs.crt"))
restart = true restart = true
@@ -769,9 +724,7 @@ private fun SettingsContent(
val userAgentTitle = stringResource(R.string.user_agent) val userAgentTitle = stringResource(R.string.user_agent)
val userAgentHelp = stringResource(R.string.user_agent_help) val userAgentHelp = stringResource(R.string.user_agent_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -799,9 +752,7 @@ private fun SettingsContent(
val uniqueContactUriTitle = stringResource(R.string.unique_contact_uri) val uniqueContactUriTitle = stringResource(R.string.unique_contact_uri)
val uniqueContactUriHelp = stringResource(R.string.unique_contact_uri_help) val uniqueContactUriHelp = stringResource(R.string.unique_contact_uri_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -826,17 +777,13 @@ private fun SettingsContent(
@Composable @Composable
fun AudioSettings(navController: NavController) { fun AudioSettings(navController: NavController) {
Row( Row(
Modifier Modifier.fillMaxWidth().padding(top = 12.dp),
.fillMaxWidth()
.padding(top = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
Text( Text(
text = stringResource(R.string.audio_settings), text = stringResource(R.string.audio_settings),
modifier = Modifier modifier = Modifier.weight(1f).clickable { navController.navigate("audio") },
.weight(1f)
.clickable { navController.navigate("audio") },
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight. Bold fontWeight = FontWeight. Bold
) )
@@ -848,9 +795,7 @@ private fun SettingsContent(
val batteryOptimizationsTitle = stringResource(R.string.battery_optimizations) val batteryOptimizationsTitle = stringResource(R.string.battery_optimizations)
val batteryOptimizationsHelp = stringResource(R.string.battery_optimizations_help) val batteryOptimizationsHelp = stringResource(R.string.battery_optimizations_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -889,9 +834,7 @@ private fun SettingsContent(
val darkThemeTitle = stringResource(R.string.dark_theme) val darkThemeTitle = stringResource(R.string.dark_theme)
val darkThemeHelp = stringResource(R.string.dark_theme_help) val darkThemeHelp = stringResource(R.string.dark_theme_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -918,9 +861,7 @@ private fun SettingsContent(
val dynamicColorsTitle = stringResource(R.string.dynamic_colors) val dynamicColorsTitle = stringResource(R.string.dynamic_colors)
val dynamicColorsHelp = stringResource(R.string.dynamic_colors_help) val dynamicColorsHelp = stringResource(R.string.dynamic_colors_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -947,9 +888,7 @@ private fun SettingsContent(
val colorBlindTitle = stringResource(R.string.colorblind) val colorBlindTitle = stringResource(R.string.colorblind)
val colorBlindHelp = stringResource(R.string.colorblind_help) val colorBlindHelp = stringResource(R.string.colorblind_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -976,9 +915,7 @@ private fun SettingsContent(
val proximitySensingTitle = stringResource(R.string.proximity_sensing) val proximitySensingTitle = stringResource(R.string.proximity_sensing)
val proximitySensingHelp = stringResource(R.string.proximity_sensing_help) val proximitySensingHelp = stringResource(R.string.proximity_sensing_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -1007,9 +944,7 @@ private fun SettingsContent(
val defaultPhoneAppHelp = stringResource(R.string.default_phone_app_help) val defaultPhoneAppHelp = stringResource(R.string.default_phone_app_help)
val dialerRoleNotAvailableMessage = stringResource(R.string.dialer_role_not_available) val dialerRoleNotAvailableMessage = stringResource(R.string.dialer_role_not_available)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -1041,14 +976,13 @@ private fun SettingsContent(
val isHeld = roleManager.isRoleHeld(RoleManager.ROLE_DIALER) val isHeld = roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
viewModel.defaultDialer.value = isHeld viewModel.defaultDialer.value = isHeld
if (isHeld) { if (isHeld) {
if (Utils.checkPermissions(ctx, arrayOf(Manifest.permission.READ_PHONE_NUMBERS))) { if (Utils.checkPermissions(ctx, arrayOf(Manifest.permission.READ_PHONE_NUMBERS)))
BaresipService.instance?.addMobileUserAgent() BaresipService.instance?.addMobileUserAgent()
} else { else
requestPermissionLauncher.launch(Manifest.permission.READ_PHONE_NUMBERS) requestPermissionLauncher.launch(Manifest.permission.READ_PHONE_NUMBERS)
}
} else {
BaresipService.instance?.addMobileUserAgent()
} }
else
BaresipService.instance?.addMobileUserAgent()
} }
Switch( Switch(
checked = defaultDialer, checked = defaultDialer,
@@ -1063,7 +997,8 @@ private fun SettingsContent(
else else
if (!roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) if (!roleManager.isRoleHeld(RoleManager.ROLE_DIALER))
dialerRoleRequest.launch(roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)) dialerRoleRequest.launch(roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER))
} else { }
else {
viewModel.defaultMessaging.value = false viewModel.defaultMessaging.value = false
try { try {
dialerRoleRequest.launch(Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS")) dialerRoleRequest.launch(Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS"))
@@ -1083,9 +1018,7 @@ private fun SettingsContent(
val defaultMessagingAppHelp = stringResource(R.string.default_messaging_app_help) val defaultMessagingAppHelp = stringResource(R.string.default_messaging_app_help)
val messagingRoleNotAvailableMessage = stringResource(R.string.messaging_role_not_available) val messagingRoleNotAvailableMessage = stringResource(R.string.messaging_role_not_available)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -1122,13 +1055,13 @@ private fun SettingsContent(
else else
if (!roleManager.isRoleHeld(RoleManager.ROLE_SMS)) if (!roleManager.isRoleHeld(RoleManager.ROLE_SMS))
messagingRoleRequest.launch(roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS)) messagingRoleRequest.launch(roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS))
} else { }
else
try { try {
messagingRoleRequest.launch(Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS")) messagingRoleRequest.launch(Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS"))
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Log.e(TAG, "ActivityNotFound exception: ${e.message}") Log.e(TAG, "ActivityNotFound exception: ${e.message}")
} }
}
} }
) )
} }
@@ -1139,9 +1072,7 @@ private fun SettingsContent(
val debugTitle = stringResource(R.string.debug) val debugTitle = stringResource(R.string.debug)
val debugHelp = stringResource(R.string.debug_help) val debugHelp = stringResource(R.string.debug_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -1168,9 +1099,7 @@ private fun SettingsContent(
val sipTraceTitle = stringResource(R.string.sip_trace) val sipTraceTitle = stringResource(R.string.sip_trace)
val sipTraceHelp = stringResource(R.string.sip_trace_help) val sipTraceHelp = stringResource(R.string.sip_trace_help)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -1199,9 +1128,7 @@ private fun SettingsContent(
val resetConfigAlert = stringResource(R.string.reset_config_alert) val resetConfigAlert = stringResource(R.string.reset_config_alert)
val resetButtonText = stringResource(R.string.reset) val resetButtonText = stringResource(R.string.reset)
Row( Row(
Modifier Modifier.fillMaxWidth().padding(end = 10.dp),
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
@@ -1222,9 +1149,7 @@ private fun SettingsContent(
dialogTitle.value = confirmationText dialogTitle.value = confirmationText
dialogMessage.value = resetConfigAlert dialogMessage.value = resetConfigAlert
firstButtonText.value = cancelButtonText firstButtonText.value = cancelButtonText
onFirstClicked.value = { onFirstClicked.value = { reset = false }
reset = false
}
lastButtonText.value = resetButtonText lastButtonText.value = resetButtonText
onLastClicked.value = { onLastClicked.value = {
Config.reset() Config.reset()
@@ -1281,10 +1206,7 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean {
val noticeTitle = ctx.getString(R.string.notice) val noticeTitle = ctx.getString(R.string.notice)
if ((Config.variable("auto_start") == "yes") != viewModel.autoStart.value) { if ((Config.variable("auto_start") == "yes") != viewModel.autoStart.value) {
Config.replaceVariable( Config.replaceVariable("auto_start", if (viewModel.autoStart.value) "yes" else "no")
"auto_start",
if (viewModel.autoStart.value) "yes" else "no"
)
save = true save = true
} }
@@ -1344,7 +1266,8 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean {
showAlert.value = true showAlert.value = true
return false return false
} }
} else { }
else {
Config.replaceVariable("dyn_dns", "yes") Config.replaceVariable("dyn_dns", "yes")
Config.updateDnsServers(BaresipService.dnsServers) Config.updateDnsServers(BaresipService.dnsServers)
} }
@@ -1353,8 +1276,7 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean {
} }
if ((Config.variable("sip_verify_server") == "yes") != viewModel.verifyServer.value) { if ((Config.variable("sip_verify_server") == "yes") != viewModel.verifyServer.value) {
Config.replaceVariable("sip_verify_server", Config.replaceVariable("sip_verify_server", if (viewModel.verifyServer.value) "yes" else "no")
if (viewModel.verifyServer.value) "yes" else "no")
Api.config_verify_server_set(viewModel.verifyServer.value) Api.config_verify_server_set(viewModel.verifyServer.value)
save = true save = true
} }
@@ -1377,8 +1299,7 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean {
} }
if ((Config.variable("sip_cuser_random") == "yes") != viewModel.uniqueContactUri.value) { if ((Config.variable("sip_cuser_random") == "yes") != viewModel.uniqueContactUri.value) {
Config.replaceVariable("sip_cuser_random", Config.replaceVariable("sip_cuser_random", if (viewModel.uniqueContactUri.value) "yes" else "no")
if (viewModel.uniqueContactUri.value) "yes" else "no")
save = true save = true
restart = true restart = true
} }
@@ -1392,25 +1313,20 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean {
Preferences(ctx).displayTheme = newDisplayTheme Preferences(ctx).displayTheme = newDisplayTheme
BaresipService.darkTheme.value = darkTheme BaresipService.darkTheme.value = darkTheme
AppCompatDelegate.setDefaultNightMode(newDisplayTheme) AppCompatDelegate.setDefaultNightMode(newDisplayTheme)
Config.replaceVariable("dark_theme", Config.replaceVariable("dark_theme", if (darkTheme) "yes" else "no")
if (darkTheme) "yes" else "no")
save = true save = true
} }
val dynamicColors = viewModel.dynamicColors.value val dynamicColors = viewModel.dynamicColors.value
if (BaresipService.dynamicColors.value != dynamicColors) { if (BaresipService.dynamicColors.value != dynamicColors) {
BaresipService.dynamicColors.value = dynamicColors BaresipService.dynamicColors.value = dynamicColors
Config.replaceVariable("dynamic_colors", Config.replaceVariable("dynamic_colors", if (dynamicColors) "yes" else "no")
if (dynamicColors) "yes" else "no")
save = true save = true
} }
val colorblind = viewModel.colorblind.value val colorblind = viewModel.colorblind.value
if ((Config.variable("colorblind") == "yes") != colorblind) { if ((Config.variable("colorblind") == "yes") != colorblind) {
Config.replaceVariable( Config.replaceVariable("colorblind", if (colorblind) "yes" else "no")
"colorblind",
if (colorblind) "yes" else "no"
)
BaresipService.colorblind = colorblind BaresipService.colorblind = colorblind
UserAgent.updateColorblindStatus() UserAgent.updateColorblindStatus()
val baresipService = Intent(ctx, BaresipService::class.java) val baresipService = Intent(ctx, BaresipService::class.java)
@@ -1421,10 +1337,7 @@ private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean {
val proximitySensing = viewModel.proximitySensing.value val proximitySensing = viewModel.proximitySensing.value
if ((Config.variable("proximity_sensing") == "yes") != proximitySensing) { if ((Config.variable("proximity_sensing") == "yes") != proximitySensing) {
Config.replaceVariable( Config.replaceVariable("proximity_sensing", if (proximitySensing) "yes" else "no")
"proximity_sensing",
if (proximitySensing) "yes" else "no"
)
BaresipService.proximitySensing = proximitySensing BaresipService.proximitySensing = proximitySensing
save = true save = true
} }
@@ -1457,9 +1370,9 @@ private fun addMissingPorts(addressList: String): String {
if (addressList == "") return "" if (addressList == "") return ""
var result = "" var result = ""
for (addr in addressList.split(",")) for (addr in addressList.split(","))
result = if (Utils.checkIpPort(addr)) { result = if (Utils.checkIpPort(addr))
"$result,$addr" "$result,$addr"
} else { else {
if (Utils.checkIpV4(addr)) if (Utils.checkIpV4(addr))
"$result,$addr:53" "$result,$addr:53"
else else