diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt index 9d78e62e..5af359ab 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt @@ -37,7 +37,9 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -108,7 +110,7 @@ private data class ScreenState( val id: Long = 0, val newId: Long = 0, val name: String = "", - val uri: String = "", + val uris: List = emptyList(), val color: Int = 0, val avatarImageUri: String? = null, val tmpAvatarFile: File? = null, @@ -138,7 +140,7 @@ private fun ContactScreen( screenState = ScreenState( new = true, name = "", - uri = uriOrNameArg, + uris = if (uriOrNameArg == "") emptyList() else listOf(uriOrNameArg), favorite = false, android = BaresipService.contactsMode == "android", color = Utils.randomColor(), @@ -153,7 +155,7 @@ private fun ContactScreen( screenState = ScreenState( new = false, name = uriOrNameArg, - uri = contact.uri, + uris = contact.uris, favorite = contact.favorite, android = false, color = contact.color, @@ -320,9 +322,9 @@ private fun ContactContent( new = screenState.new, onNameChange = { newName -> onStateChange(screenState.copy(name = newName)) } ) - ContactUri( - uri = screenState.uri, - onUriChange = { newUri -> onStateChange(screenState.copy(uri = newUri)) } + ContactUris( + uris = screenState.uris, + onUrisChange = { newUris -> onStateChange(screenState.copy(uris = newUris)) } ) Favorite( ctx = ctx, @@ -456,16 +458,62 @@ private fun ContactName(name: String, new: Boolean, onNameChange: (String) -> Un } @Composable -private fun ContactUri(uri: String, onUriChange: (String) -> Unit) { - OutlinedTextField( - value = uri, - placeholder = { Text(stringResource(R.string.user_domain_or_number)) }, - onValueChange = onUriChange, +private fun ContactUris(uris: List, onUrisChange: (List) -> Unit) { + Column( modifier = Modifier.fillMaxWidth(), - textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), - label = { Text(stringResource(R.string.sip_or_tel_uri)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) - ) + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + uris.forEachIndexed { index, uri -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = uri, + placeholder = { Text(stringResource(R.string.user_domain_or_number)) }, + onValueChange = { newUri -> + val newList = uris.toMutableList() + newList[index] = newUri + onUrisChange(newList.toList()) + }, + modifier = Modifier.weight(1f), + textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), + label = { Text("${stringResource(R.string.sip_or_tel_uri)} ${index + 1}") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) + ) + if (uris.size > 1) { + IconButton( + onClick = { + val newList = uris.toMutableList() + newList.removeAt(index) + onUrisChange(newList.toList()) + }, + modifier = Modifier.padding(start = 4.dp) + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = "Delete", + tint = MaterialTheme.colorScheme.error + ) + } + } + } + } + IconButton( + onClick = { + val newList = uris.toMutableList() + newList.add("") + onUrisChange(newList.toList()) + }, + modifier = Modifier.align(Alignment.Start) + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "Add", + tint = MaterialTheme.colorScheme.primary + ) + } + } } @Composable @@ -520,22 +568,34 @@ private fun checkOnClick( uriOrNameArg: String ): Boolean { - var newUri = currentState.uri.filterNot{setOf('-', ' ', '(', ')').contains(it)} - if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:")) - newUri = if (Utils.isTelNumber(newUri)) - "tel:$newUri" - else - "sip:$newUri" + val newUris = ArrayList() + for (uri in currentState.uris) { + var u = uri.filterNot{setOf('-', ' ', '(', ')').contains(it)} + if (u == "") continue + if (!u.startsWith("sip:") && !u.startsWith("tel:")) + u = if (Utils.isTelNumber(u)) + "tel:$u" + else + "sip:$u" - if (!Utils.checkUri(newUri)) { + if (!Utils.checkUri(u)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), u) + showAlert.value = true + return false + } + newUris.add(u) + } + + if (newUris.isEmpty()) { alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), newUri) + alertMessage.value = ctx.getString(R.string.sip_or_tel_uri) showAlert.value = true return false } var newName = currentState.name.trim() - if (newName == "") newName = newUri.substringAfter(":") + if (newName == "") newName = newUris[0].substringAfter(":") if (!Utils.checkName(newName)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_contact), newName) @@ -572,7 +632,7 @@ private fun checkOnClick( val contact: Contact.BaresipContact = Contact.BaresipContact( newName, - newUri, + newUris, currentState.color, idToUse, currentState.favorite @@ -683,17 +743,19 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo .withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name) .build()) - val mimeType = if (contact.uri.startsWith("sip:")) - CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE - else - CommonDataKinds.Phone.CONTENT_ITEM_TYPE - ops.add( - ContentProviderOperation - .newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(Data.RAW_CONTACT_ID, 0) - .withValue(Data.MIMETYPE, mimeType) - .withValue(Data.DATA1, contact.uri.substringAfter(":")) - .build()) + for (uri in contact.uris) { + val mimeType = if (uri.startsWith("sip:")) + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + else + CommonDataKinds.Phone.CONTENT_ITEM_TYPE + ops.add( + ContentProviderOperation + .newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(Data.RAW_CONTACT_ID, 0) + .withValue(Data.MIMETYPE, mimeType) + .withValue(Data.DATA1, uri.substringAfter(":")) + .build()) + } if (contact.avatarImage != null) { val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!) @@ -717,8 +779,9 @@ private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Bo } private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) { - if (updateAndroidUri(ctx, rawContactId, contact.uri) == 0) - addAndroidUri(ctx, rawContactId, contact.uri) + for (uri in contact.uris) + if (updateAndroidUri(ctx, rawContactId, uri) == 0) + addAndroidUri(ctx, rawContactId, uri) if (updateAndroidPhoto(ctx, rawContactId, contact.avatarImage) == 0) if (contact.avatarImage != null) addAndroidPhoto(ctx, rawContactId, contact.avatarImage!!) diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt index 869b885d..616875f5 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt @@ -72,6 +72,7 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import coil.compose.AsyncImage import com.tutpro.baresip.CustomElements.AlertDialog +import com.tutpro.baresip.CustomElements.SelectableAlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar fun NavGraphBuilder.callsScreenRoute(navController: NavController, viewModel: ViewModel) { @@ -303,6 +304,15 @@ private fun Calls( lastButtonText = stringResource(R.string.ok), ) + SelectableAlertDialog( + openDialog = CustomElements.showSelectItemDialog, + title = stringResource(R.string.choose_destination_uri), + items = CustomElements.selectItems.value, + onItemClicked = CustomElements.selectItemAction.value, + neutralButtonText = stringResource(R.string.cancel), + onNeutralClicked = {} + ) + val lazyListState = rememberLazyListState() LazyColumn( modifier = Modifier @@ -339,32 +349,88 @@ private fun Calls( secondButtonText.value = ctx.getString(R.string.call) secondAction.value = { if (ua != null) { - handleIntent(ctx, viewModel, intent, "call") - navController.navigate("main") { - popUpTo("main") - launchSingleTop = true + val contact = Contact.findContact(peerUri) + val uris = if (contact is Contact.BaresipContact) { + if (ua.account.isMobile) + contact.uris.filter { it.startsWith("tel:") } + else + contact.uris + } else { + listOf(peerUri) + } + + if (uris.size > 1) { + CustomElements.selectItems.value = uris + CustomElements.selectItemAction.value = { index -> + intent.putExtra("peer", uris[index]) + handleIntent(ctx, viewModel, intent, "call") + navController.navigate("main") { + popUpTo("main") + launchSingleTop = true + } + CustomElements.showSelectItemDialog.value = false + } + CustomElements.showSelectItemDialog.value = true + } else if (uris.size == 1) { + intent.putExtra("peer", uris[0]) + handleIntent(ctx, viewModel, intent, "call") + navController.navigate("main") { + popUpTo("main") + launchSingleTop = true + } } } } lastButtonText.value = ctx.getString(R.string.send_message) lastAction.value = { - if (account.isMobile) { - if (!Utils.isDefaultSmsApp(ctx)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = ctx.getString(R.string.enable_default_messaging) - showAlert.value = true + if (ua != null) { + val contact = Contact.findContact(peerUri) + val uris = if (contact is Contact.BaresipContact) { + if (ua.account.isMobile) + contact.uris.filter { it.startsWith("tel:") } + else + contact.uris } else { - if (ua != null) { + listOf(peerUri) + } + + if (uris.size > 1) { + CustomElements.selectItems.value = uris + CustomElements.selectItemAction.value = { index -> + intent.putExtra("peer", uris[index]) + if (ua.account.isMobile) { + if (!Utils.isDefaultSmsApp(ctx)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.enable_default_messaging) + showAlert.value = true + } else { + handleIntent(ctx, viewModel, intent, "message") + navController.navigateUp() + } + } else { + handleIntent(ctx, viewModel, intent, "message") + navController.navigateUp() + } + CustomElements.showSelectItemDialog.value = false + } + CustomElements.showSelectItemDialog.value = true + } else if (uris.size == 1) { + intent.putExtra("peer", uris[0]) + if (ua.account.isMobile) { + if (!Utils.isDefaultSmsApp(ctx)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.enable_default_messaging) + showAlert.value = true + } else { + handleIntent(ctx, viewModel, intent, "message") + navController.navigateUp() + } + } else { handleIntent(ctx, viewModel, intent, "message") navController.navigateUp() } } } - else - if (ua != null) { - handleIntent(ctx, viewModel, intent, "message") - navController.navigateUp() - } } showDialog.value = true }, diff --git a/app/src/main/kotlin/com/tutpro/baresip/Contact.kt b/app/src/main/kotlin/com/tutpro/baresip/Contact.kt index 7d1b03e5..4a97063c 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Contact.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Contact.kt @@ -13,7 +13,7 @@ import java.util.ArrayList sealed class Contact { - class BaresipContact(var name: String, var uri: String, var color: Int, var id: Long, + class BaresipContact(var name: String, val uris: ArrayList, var color: Int, var id: Long, var favorite: Boolean): Contact() { var avatarImage: Bitmap? = null } @@ -57,7 +57,7 @@ sealed class Contact { fun copy(): Contact { val copy = when (this) { is BaresipContact -> - BaresipContact(name, uri, color, id, favorite) + BaresipContact(name, ArrayList(uris), color, id, favorite) is AndroidContact -> AndroidContact(name, color, thumbnailUri, id, favorite) } @@ -107,9 +107,10 @@ sealed class Contact { when (c) { is BaresipContact -> { if (c.name.equals(name, ignoreCase = true)) { - uris.add(c.uri.removePrefix("<") - .replaceAfter(">", "") - .replace(">", "")) + for (u in c.uris) + uris.add(u.removePrefix("<") + .replaceAfter(">", "") + .replace(">", "")) return uris } } @@ -128,8 +129,9 @@ sealed class Contact { for (c in BaresipService.contacts) when (c) { is BaresipContact -> { - if (Utils.uriMatch(c.uri, uri)) - return c + for (u in c.uris) + if (Utils.uriMatch(u, uri)) + return c } is AndroidContact -> { val cleanUri = uri.filterNot{setOf('-', ' ', '(', ')').contains(it)} @@ -153,7 +155,7 @@ sealed class Contact { val avatarFiles = avatarFileNames() var contents = "" for (c in BaresipService.baresipContacts.value) { - contents += "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}" + + contents += "\"${c.name}\" <${c.uris.joinToString(",")}>;id=${c.id};color=${c.color}" + ";favorite=${if (c.favorite) "yes" else "no"}\n" avatarFiles.remove(c.id.toString() + ".png") } @@ -236,7 +238,8 @@ sealed class Contact { contactNo++ val name = parts[1] val uriParams = parts[2].trim() - val uri = uriParams.substringAfter("<").substringBefore(">") + val urisPart = uriParams.substringAfter("<").substringBefore(">") + val uris = ArrayList(urisPart.split(",")) val params = uriParams.substringAfter(">;") val colorValue = Utils.paramValue(params, "color" ) val color: Int = if (colorValue != "") @@ -249,8 +252,8 @@ sealed class Contact { else baseId + contactNo val favorite = Utils.paramValue(params, "favorite" ) == "yes" - Log.d(TAG, "Restoring contact $name, $uri, $color, $id") - val contact = BaresipContact(name, uri, color, id, favorite) + Log.d(TAG, "Restoring contact $name, $urisPart, $color, $id") + val contact = BaresipContact(name, uris, color, id, favorite) val avatarFilePath = BaresipService.filesPath + "/$id.png" if (File(avatarFilePath).exists()) { try { diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt index 03e8a089..f113889b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt @@ -77,6 +77,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.tutpro.baresip.CustomElements.AlertDialog +import com.tutpro.baresip.CustomElements.SelectableAlertDialog import com.tutpro.baresip.CustomElements.TextAvatar import com.tutpro.baresip.CustomElements.verticalScrollbar import java.io.File @@ -209,6 +210,15 @@ private fun ContactsScreen( } ) + SelectableAlertDialog( + openDialog = CustomElements.showSelectItemDialog, + title = stringResource(R.string.choose_destination_uri), + items = CustomElements.selectItems.value, + onItemClicked = CustomElements.selectItemAction.value, + neutralButtonText = stringResource(R.string.cancel), + onNeutralClicked = {} + ) + Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, @@ -508,7 +518,6 @@ private fun ContactsContent( val intent = Intent(ctx, MainActivity::class.java) if (ua != null) { intent.putExtra("uap", ua.uap) - intent.putExtra("peer", contact.uri) } else Log.w(TAG, "onClickListener did not find UA for $aor") @@ -519,30 +528,79 @@ private fun ContactsContent( secondText.value = ctx.getString(R.string.call) secondAction.value = { if (ua != null) { - handleIntent(ctx, viewModel, intent, "call") - navController.navigate("main") { - popUpTo("main") - launchSingleTop = true + val uris = if (ua.account.isMobile) + contact.uris.filter { it.startsWith("tel:") } + else + contact.uris + + if (uris.size > 1) { + CustomElements.selectItems.value = uris + CustomElements.selectItemAction.value = { index: Int -> + intent.putExtra("peer", uris[index]) + handleIntent(ctx, viewModel, intent, "call") + navController.navigate("main") { + popUpTo("main") + launchSingleTop = true + } + CustomElements.showSelectItemDialog.value = false + } + CustomElements.showSelectItemDialog.value = true + } else if (uris.size == 1) { + intent.putExtra("peer", uris[0]) + handleIntent(ctx, viewModel, intent, "call") + navController.navigate("main") { + popUpTo("main") + launchSingleTop = true + } } } } lastText.value = ctx.getString(R.string.send_message) lastAction.value = { if (ua != null) { - if (ua.account.isMobile) { - if (!Utils.isDefaultSmsApp(ctx)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = ctx.getString(R.string.enable_default_messaging) - showAlert.value = true - } else { + val uris = if (ua.account.isMobile) + contact.uris.filter { it.startsWith("tel:") } + else + contact.uris + + if (uris.size > 1) { + CustomElements.selectItems.value = uris + CustomElements.selectItemAction.value = { index: Int -> + intent.putExtra("peer", uris[index]) + if (ua.account.isMobile) { + if (!Utils.isDefaultSmsApp(ctx)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.enable_default_messaging) + showAlert.value = true + } else { + handleIntent(ctx, viewModel, intent, "message") + navController.navigateUp() + } + } + else { + handleIntent(ctx, viewModel, intent, "message") + navController.navigateUp() + } + CustomElements.showSelectItemDialog.value = false + } + CustomElements.showSelectItemDialog.value = true + } else if (uris.size == 1) { + intent.putExtra("peer", uris[0]) + if (ua.account.isMobile) { + if (!Utils.isDefaultSmsApp(ctx)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.enable_default_messaging) + showAlert.value = true + } else { + handleIntent(ctx, viewModel, intent, "message") + navController.navigateUp() + } + } + else { handleIntent(ctx, viewModel, intent, "message") navController.navigateUp() } } - else { - handleIntent(ctx, viewModel, intent, "message") - navController.navigateUp() - } } } showDialog.value = true diff --git a/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt b/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt index b18c9da1..e6258352 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt @@ -82,6 +82,10 @@ import androidx.compose.ui.window.DialogProperties object CustomElements { + val selectItems = mutableStateOf(listOf()) + val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} }) + val showSelectItemDialog = mutableStateOf(false) + @Composable fun Button( onClick: () -> Unit, @@ -424,7 +428,7 @@ object CustomElements { .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer + containerColor = MaterialTheme.colorScheme.surfaceVariant ) ) { Column(modifier = Modifier.padding(16.dp)) { @@ -449,8 +453,8 @@ object CustomElements { modifier = Modifier.fillMaxWidth() ) { Text( - text = item, - color = MaterialTheme.colorScheme.onSurface, + text = stringResource(R.string.bullet_item, item), + color = MaterialTheme.colorScheme.primary, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Start ) diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 92bcefb7..67eef158 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -6,7 +6,6 @@ import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.app.Activity import android.app.Activity.RESULT_OK import android.app.KeyguardManager -import android.app.role.RoleManager import android.content.Context import android.content.Intent import android.content.res.Configuration @@ -180,9 +179,9 @@ private val showPasswordsDialog = mutableStateOf(false) private var passwordAccounts = mutableListOf() private var password = mutableStateOf("") -private val selectItems = mutableStateOf(listOf()) -private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} }) -private val showSelectItemDialog = mutableStateOf(false) +private val selectItems = CustomElements.selectItems +private val selectItemAction = CustomElements.selectItemAction +private val showSelectItemDialog = CustomElements.showSelectItemDialog fun NavGraphBuilder.mainScreenRoute( navController: NavController,