Renamed AndroidContactScreen to ContactScreen and use it for both contact types

This commit is contained in:
Juha Heinanen
2026-05-19 06:46:52 +03:00
parent b12d657ee1
commit 5ebfccfe8c
6 changed files with 230 additions and 257 deletions
@@ -46,6 +46,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -54,6 +55,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -495,23 +497,26 @@ private fun ContactUris(uris: List<String>, onUrisChange: (List<String>) -> Unit
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
) )
if (uris.size > 1) { if (uris.size > 1) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
IconButton( IconButton(
onClick = { onClick = {
val newList = uris.toMutableList() val newList = uris.toMutableList()
newList.removeAt(index) newList.removeAt(index)
onUrisChange(newList.toList()) onUrisChange(newList.toList())
}, },
modifier = Modifier.padding(start = 4.dp) modifier = Modifier.padding(start = 2.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Remove, imageVector = Icons.Filled.Remove,
contentDescription = "Remove", contentDescription = "Remove",
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(32.dp)
) )
} }
} }
} }
} }
}
IconButton( IconButton(
onClick = { onClick = {
val newList = uris.toMutableList() val newList = uris.toMutableList()
@@ -104,6 +104,7 @@ class BaresipService: Service() {
private lateinit var wifiLock: WifiManager.WifiLock private lateinit var wifiLock: WifiManager.WifiLock
private lateinit var bluetoothReceiver: BroadcastReceiver private lateinit var bluetoothReceiver: BroadcastReceiver
private lateinit var hotSpotReceiver: BroadcastReceiver private lateinit var hotSpotReceiver: BroadcastReceiver
private lateinit var airplaneModeReceiver: BroadcastReceiver
private lateinit var androidContactsObserver: ContentObserver private lateinit var androidContactsObserver: ContentObserver
private lateinit var networkCallback: ConnectivityManager.NetworkCallback private lateinit var networkCallback: ConnectivityManager.NetworkCallback
private lateinit var stopState: String private lateinit var stopState: String
@@ -120,6 +121,7 @@ class BaresipService: Service() {
private var androidContactsObserverRegistered = false private var androidContactsObserverRegistered = false
private var hotSpotReceiverRegistered = false private var hotSpotReceiverRegistered = false
private var bluetoothReceiverRegistered = false private var bluetoothReceiverRegistered = false
private var airplaneModeReceiverRegistered = false
private var isNotificationInCall = false private var isNotificationInCall = false
private var isServiceClean = false private var isServiceClean = false
private var cleanupRunnable: Runnable? = null private var cleanupRunnable: Runnable? = null
@@ -288,6 +290,23 @@ class BaresipService: Service() {
) )
hotSpotReceiverRegistered = true hotSpotReceiverRegistered = true
airplaneModeReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
val isAirplaneModeOn = intent.getBooleanExtra("state", false)
Log.d(TAG, "Airplane mode changed: $isAirplaneModeOn")
updateMobileStatus(isAirplaneModeOn)
}
}
}
ContextCompat.registerReceiver(
this,
airplaneModeReceiver,
IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
ContextCompat.RECEIVER_EXPORTED
)
airplaneModeReceiverRegistered = true
tm = getSystemService(TELECOM_SERVICE) as TelecomManager tm = getSystemService(TELECOM_SERVICE) as TelecomManager
btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
@@ -2027,6 +2046,8 @@ class BaresipService: Service() {
account.regint = 0 account.regint = 0
account.telProvider = "" account.telProvider = ""
val mobileUa = UserAgent(0L, account) val mobileUa = UserAgent(0L, account)
val isAirplaneModeOn = Settings.Global.getInt(contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0
mobileUa.status = if (isAirplaneModeOn) R.drawable.circle_white else circleGreen.getValue(colorblind)
val updatedUas = uas.value.toMutableList() val updatedUas = uas.value.toMutableList()
updatedUas.add(mobileUa) updatedUas.add(mobileUa)
@@ -2042,6 +2063,19 @@ class BaresipService: Service() {
} }
} }
private fun updateMobileStatus(isAirplaneModeOn: Boolean) {
uas.value.find { it.account.isMobile }?.let { ua ->
val status = if (isAirplaneModeOn)
R.drawable.circle_white
else
circleGreen.getValue(colorblind)
ua.updateStatus(status)
updateStatusNotification()
if (isMainVisible)
registrationUpdate.postValue(System.currentTimeMillis())
}
}
@SuppressLint("FullScreenIntentPolicy") @SuppressLint("FullScreenIntentPolicy")
fun handleIncomingCall(call: Call) { fun handleIncomingCall(call: Call) {
val ua = call.ua val ua = call.ua
@@ -2635,6 +2669,12 @@ class BaresipService: Service() {
bluetoothReceiverRegistered = false bluetoothReceiverRegistered = false
} catch (_: IllegalArgumentException) {} } catch (_: IllegalArgumentException) {}
} }
if (airplaneModeReceiverRegistered) {
try {
unregisterReceiver(airplaneModeReceiver)
airplaneModeReceiverRegistered = false
} catch (_: IllegalArgumentException) {}
}
val callps = ConnectionService.connections.keys.toList() val callps = ConnectionService.connections.keys.toList()
for (callp in callps) for (callp in callps)
ConnectionService.onCallClosed(callp) ConnectionService.onCallClosed(callp)
@@ -13,45 +13,38 @@ import java.util.ArrayList
sealed class Contact { sealed class Contact {
abstract fun name(): String
abstract fun id(): Long
abstract fun favorite(): Boolean
abstract fun colorInt(): Int
abstract fun uris(): List<String>
abstract fun email(): String
class BaresipContact(var name: String, val uris: ArrayList<String>, var email: String, var color: Int, class BaresipContact(var name: String, val uris: ArrayList<String>, var email: String, var color: Int,
var id: Long, var favorite: Boolean): Contact() { var id: Long, var favorite: Boolean): Contact() {
var avatarImage: Bitmap? = null var avatarImage: Bitmap? = null
override fun name() = name
override fun id() = id
override fun favorite() = favorite
override fun colorInt() = color
override fun uris() = uris
override fun email() = email
} }
class AndroidContact(var name: String, var color: Int, var thumbnailUri: Uri?, class AndroidContact(var name: String, val uris: ArrayList<String>, var email: String, var color: Int,
var id: Long, var favorite: Boolean): Contact() { var thumbnailUri: Uri?, var id: Long, var favorite: Boolean): Contact() {
val uris = ArrayList<String>() override fun name() = name
} override fun id() = id
override fun favorite() = favorite
fun name(): String { override fun colorInt() = color
return when (this) { override fun uris() = uris
is AndroidContact -> name override fun email() = email
is BaresipContact -> name
}
}
fun id(): Long {
return when (this) {
is AndroidContact -> id
is BaresipContact -> id
}
}
fun favorite(): Boolean {
return when (this) {
is AndroidContact -> favorite
is BaresipContact -> favorite
}
} }
fun color(): String { fun color(): String {
val intColor = when (this) {
is AndroidContact -> color
is BaresipContact -> color
}
// Mask with 0xFFFFFF to ensure we get the standard 6-character hex code (RRGGBB) // Mask with 0xFFFFFF to ensure we get the standard 6-character hex code (RRGGBB)
// ignoring the alpha channel for compatibility with simple string parsers. // ignoring the alpha channel for compatibility with simple string parsers.
return String.format("#%06X", 0xFFFFFF and intColor) return String.format("#%06X", 0xFFFFFF and colorInt())
} }
fun copy(): Contact { fun copy(): Contact {
@@ -59,14 +52,10 @@ sealed class Contact {
is BaresipContact -> is BaresipContact ->
BaresipContact(name, ArrayList(uris), email, color, id, favorite) BaresipContact(name, ArrayList(uris), email, color, id, favorite)
is AndroidContact -> is AndroidContact ->
AndroidContact(name, color, thumbnailUri, id, favorite) AndroidContact(name, ArrayList(uris), email, color, thumbnailUri, id, favorite)
} }
when (this) { if (this is BaresipContact)
is BaresipContact ->
(copy as BaresipContact).avatarImage = this.avatarImage (copy as BaresipContact).avatarImage = this.avatarImage
is AndroidContact ->
(copy as AndroidContact).uris.addAll(this.uris)
}
return copy return copy
} }
@@ -175,7 +164,8 @@ sealed class Contact {
ContactsContract.Contacts.STARRED) ContactsContract.Contacts.STARRED)
val selection = val selection =
ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "' OR " + ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "' OR " +
ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'" ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "' OR " +
ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'"
val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection,
selection, null, null) selection, null, null)
BaresipService.androidContacts.value = listOf() BaresipService.androidContacts.value = listOf()
@@ -184,27 +174,33 @@ sealed class Contact {
val id = cur.getLong(0) val id = cur.getLong(0)
val name = cur.getString(1) ?: "" val name = cur.getString(1) ?: ""
val mime = cur.getString(2) val mime = cur.getString(2)
val data = cur.getString(3) val data = cur.getString(3) ?: continue
val thumb = cur.getString(4)?.toUri() val thumb = cur.getString(4)?.toUri()
val starred = cur.getInt(5) val starred = cur.getInt(5)
val contact = if (contacts.containsKey(id)) val contact = if (contacts.containsKey(id))
contacts[id]!! contacts[id]!!
else else
AndroidContact(name, Utils.randomColor(), thumb, id, starred == 1) AndroidContact(name, ArrayList<String>(), "", Utils.randomColor(), thumb, id, starred == 1)
if (contact.name == "" && name != "") if (contact.name == "" && name != "")
contact.name = name contact.name = name
if (contact.thumbnailUri == null && thumb != null) if (contact.thumbnailUri == null && thumb != null)
contact.thumbnailUri = thumb contact.thumbnailUri = thumb
if (mime == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) { when (mime) {
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
val uri = "tel:${data.filterNot { setOf('-', ' ', '(', ')').contains(it) }}" val uri = "tel:${data.filterNot { setOf('-', ' ', '(', ')').contains(it) }}"
if (uri !in contact.uris) if (uri !in contact.uris)
contact.uris.add(uri) contact.uris.add(uri)
// contact.types.add(typeToString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE)))
} }
else if (mime == ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE) ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> {
contact.uris.add("sip:$data") val uri = "sip:$data"
else if (uri !in contact.uris)
continue contact.uris.add(uri)
}
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> {
if (contact.email == "")
contact.email = data
}
}
if (!contacts.containsKey(id)) if (!contacts.containsKey(id))
contacts[id] = contact contacts[id] = contact
} }
@@ -2,8 +2,10 @@ package com.tutpro.baresip
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -18,14 +20,14 @@ import androidx.compose.foundation.layout.imePadding
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.statusBars
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
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
import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material.icons.automirrored.filled.Chat
import androidx.compose.material.icons.outlined.Call import androidx.compose.material.icons.outlined.Call
import androidx.compose.material.icons.outlined.Email
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -40,10 +42,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -52,10 +56,11 @@ import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import coil.compose.AsyncImage import coil.compose.AsyncImage
import androidx.core.net.toUri
fun NavGraphBuilder.androidContactScreenRoute(navController: NavController, viewModel: ViewModel) { fun NavGraphBuilder.contactScreenRoute(navController: NavController, viewModel: ViewModel) {
composable( composable(
route = "android_contact/{name}", route = "contact/{name}",
arguments = listOf(navArgument("name") { type = NavType.StringType }) arguments = listOf(navArgument("name") { type = NavType.StringType })
) { backStackEntry -> ) { backStackEntry ->
val ctx = LocalContext.current val ctx = LocalContext.current
@@ -71,10 +76,11 @@ fun NavGraphBuilder.androidContactScreenRoute(navController: NavController, view
@Composable @Composable
private fun ContactScreen(ctx: Context, viewModel: ViewModel, navController: NavController, name: String) { private fun ContactScreen(ctx: Context, viewModel: ViewModel, navController: NavController, name: String) {
val contact = Contact.androidContact(name) val contact = Contact.baresipContact(name) ?: Contact.androidContact(name)
if (contact == null) { if (contact == null) {
Log.e(TAG, "No Android contact found with name $name") Log.e(TAG, "No contact found with name $name")
navController.navigateUp() navController.navigateUp()
return
} }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier.fillMaxSize().imePadding(),
@@ -90,7 +96,7 @@ private fun ContactScreen(ctx: Context, viewModel: ViewModel, navController: Nav
} }
}, },
content = { contentPadding -> content = { contentPadding ->
ContactContent(ctx, viewModel, navController, contentPadding, contact!!) ContactContent(ctx, viewModel, navController, contentPadding, contact)
} }
) )
} }
@@ -123,19 +129,23 @@ private fun ContactContent(
viewModel: ViewModel, viewModel: ViewModel,
navController: NavController, navController: NavController,
contentPadding: PaddingValues, contentPadding: PaddingValues,
contact: Contact.AndroidContact contact: Contact
) { ) {
val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.padding(contentPadding) .padding(contentPadding)
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp), .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Avatar(contact) Avatar(contact)
ContactName(contact.name) ContactName(contact.name())
Uris(ctx, viewModel, navController, contact) Uris(ctx, viewModel, navController, contact)
if (contact.email().isNotEmpty())
Email(ctx, contact.email())
} }
} }
@@ -153,7 +163,19 @@ private fun TextAvatar(text: String, color: Int) {
} }
@Composable @Composable
private fun ImageAvatar(uri: Uri) { private fun ImageAvatar(bitmap: Bitmap) {
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = "Avatar",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(avatarSize.dp)
.clip(CircleShape)
)
}
@Composable
private fun AsyncImageAvatar(uri: Uri) {
AsyncImage( AsyncImage(
model = uri, model = uri,
contentDescription = stringResource(R.string.avatar_image), contentDescription = stringResource(R.string.avatar_image),
@@ -163,20 +185,29 @@ private fun ImageAvatar(uri: Uri) {
} }
@Composable @Composable
private fun Avatar(contact: Contact.AndroidContact) { private fun Avatar(contact: Contact) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
val color = contact.color val color = contact.colorInt()
val name = contact.name val name = contact.name()
val thumbnailUri = contact.thumbnailUri when (contact) {
if (thumbnailUri != null) is Contact.BaresipContact -> {
ImageAvatar(thumbnailUri) if (contact.avatarImage != null)
ImageAvatar(contact.avatarImage!!)
else else
TextAvatar(if (name == "") "" else name[0].toString(), color) TextAvatar(if (name == "") "" else name[0].toString(), color)
} }
is Contact.AndroidContact -> {
if (contact.thumbnailUri != null)
AsyncImageAvatar(contact.thumbnailUri!!)
else
TextAvatar(if (name == "") "" else name[0].toString(), color)
}
}
}
} }
@Composable @Composable
@@ -184,9 +215,48 @@ private fun ContactName(name: String) {
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,
horizontalArrangement = Arrangement.Center
) {
Text(
text = name,
fontSize = 24.sp,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun Email(ctx: Context, email: String) {
Row(
Modifier.fillMaxWidth().padding(bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
Text(name, fontSize = 24.sp, color = MaterialTheme.colorScheme.onBackground) Text(
text = stringResource(R.string.bullet_item, email),
modifier = Modifier.weight(1f),
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground,
)
IconButton(
onClick = {
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:$email".toUri()
}
try {
ctx.startActivity(emailIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to start email activity: ${e.message}")
}
}
) {
Icon(
imageVector = Icons.Outlined.Email,
contentDescription = "Send Email",
tint = MaterialTheme.colorScheme.onBackground
)
}
} }
} }
@@ -195,23 +265,18 @@ private fun Uris(
ctx: Context, ctx: Context,
viewModel: ViewModel, viewModel: ViewModel,
navController: NavController, navController: NavController,
contact: Contact.AndroidContact contact: Contact
) { ) {
val lazyListState = rememberLazyListState() Column(
LazyColumn( modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 4.dp)
.background(MaterialTheme.colorScheme.background),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
items(contact.uris) { uri -> for (uri in contact.uris()) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = uri.substringAfter(":"), text = stringResource(R.string.bullet_item, uri.substringAfter(":")),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
fontSize = 18.sp, fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
@@ -225,6 +290,10 @@ private fun Uris(
if (ua == null) if (ua == null)
Log.w(TAG, "Message clickable did not find AoR $aor") Log.w(TAG, "Message clickable did not find AoR $aor")
else { else {
if (ua.account.isMobile && !Utils.isDefaultSmsApp(ctx)) {
handleDialog(ctx, ctx.getString(R.string.notice),
ctx.getString(R.string.enable_default_messaging))
} 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)
@@ -235,6 +304,7 @@ private fun Uris(
} }
} }
} }
}
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.Chat, imageVector = Icons.AutoMirrored.Filled.Chat,
@@ -272,5 +342,3 @@ private fun Uris(
} }
} }
} }
@@ -84,25 +84,18 @@ import com.tutpro.baresip.CustomElements.TextAvatar
import com.tutpro.baresip.CustomElements.verticalScrollbar import com.tutpro.baresip.CustomElements.verticalScrollbar
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import androidx.core.net.toUri
const val avatarSize: Int = 96 const val avatarSize: Int = 96
fun NavGraphBuilder.contactsScreenRoute( fun NavGraphBuilder.contactsScreenRoute(navController: NavController) {
navController: NavController,
viewModel: ViewModel
) {
composable("contacts") { _ -> composable("contacts") { _ ->
ContactsScreen(navController = navController, viewModel = viewModel) ContactsScreen(navController = navController)
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ContactsScreen( private fun ContactsScreen(navController: NavController) {
navController: NavController,
viewModel: ViewModel
) {
val ctx = LocalContext.current val ctx = LocalContext.current
val activity = LocalActivity.current!! val activity = LocalActivity.current!!
@@ -429,7 +422,7 @@ private fun ContactsScreen(
) )
}, },
content = { contentPadding -> content = { contentPadding ->
ContactsContent(ctx, viewModel, navController, contentPadding, searchContactName) ContactsContent(ctx, navController, contentPadding, searchContactName)
} }
) )
} }
@@ -497,31 +490,21 @@ private fun BottomBar(
@Composable @Composable
private fun ContactsContent( private fun ContactsContent(
ctx: Context, ctx: Context,
viewModel: ViewModel,
navController: NavController, navController: NavController,
contentPadding: PaddingValues, contentPadding: PaddingValues,
searchQuery: String searchQuery: String
) { ) {
val showDialog = remember { mutableStateOf(false) } val showConfirmationDialog = remember { mutableStateOf(false) }
val dialogMessage = remember { mutableStateOf("") } val confirmationDialogMessage = remember { mutableStateOf("") }
val secondText = remember { mutableStateOf("") }
val secondAction = remember { mutableStateOf({}) }
val lastText = remember { mutableStateOf("") }
val lastAction = remember { mutableStateOf({}) } val lastAction = remember { mutableStateOf({}) }
val thirdText = remember { mutableStateOf("") }
val thirdAction = remember { mutableStateOf({}) }
if (showDialog.value) if (showConfirmationDialog.value)
AlertDialog( AlertDialog(
showDialog = showDialog, showDialog = showConfirmationDialog,
title = stringResource(R.string.confirmation), title = stringResource(R.string.confirmation),
message = dialogMessage.value, message = confirmationDialogMessage.value,
firstButtonText = stringResource(R.string.cancel), firstButtonText = stringResource(R.string.cancel),
secondButtonText = secondText.value, lastButtonText = stringResource(R.string.delete),
onSecondClicked = secondAction.value,
thirdButtonText = thirdText.value,
onThirdClicked = thirdAction.value,
lastButtonText = lastText.value,
onLastClicked = lastAction.value, onLastClicked = lastAction.value,
) )
@@ -638,132 +621,13 @@ private fun ContactsContent(
.padding(start = 10.dp) .padding(start = 10.dp)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
val aor = viewModel.selectedAor.value navController.navigate("contact/${contact.name()}")
val ua = UserAgent.ofAor(aor)
val intent = Intent(ctx, MainActivity::class.java)
if (ua != null) {
intent.putExtra("uap", ua.uap)
}
else
Log.w(TAG, "onClickListener did not find UA for $aor")
val peerName = contact.name()
if (contact.email.isNotEmpty())
dialogMessage.value = String.format(
ctx.getString(R.string.contact_email_action_question),
peerName
)
else
dialogMessage.value = String.format(
ctx.getString(R.string.contact_action_question),
peerName
)
secondText.value = ctx.getString(R.string.call)
secondAction.value = {
if (ua != null) {
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
}
}
else {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.no_telephone_number)
showAlert.value = true
}
}
}
thirdText.value = ctx.getString(R.string.send_message)
thirdAction.value = {
if (ua != null) {
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()
}
}
}
}
if (contact.email.isNotEmpty()) {
lastText.value = ctx.getString(R.string.send_email)
lastAction.value = {
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:${contact.email}".toUri()
}
try {
ctx.startActivity(emailIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to start email activity: ${e.message}")
}
}
} else {
lastText.value = ""
}
showDialog.value = true
}, },
onLongClick = { onLongClick = {
dialogMessage.value = String.format( confirmationDialogMessage.value = String.format(
ctx.getString(R.string.contact_delete_question), ctx.getString(R.string.contact_delete_question),
contact.name() contact.name()
) )
secondText.value = ""
lastText.value = ctx.getString(R.string.delete)
lastAction.value = { lastAction.value = {
val id = contact.id val id = contact.id
val avatarFile = File( val avatarFile = File(
@@ -782,7 +646,7 @@ private fun ContactsContent(
} }
Contact.removeBaresipContact(contact) Contact.removeBaresipContact(contact)
} }
showDialog.value = true showConfirmationDialog.value = true
} }
) )
) )
@@ -808,14 +672,14 @@ 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 = { navController.navigate("android_contact/${contact.name()}") }, onClick = {
navController.navigate("contact/${contact.name()}")
},
onLongClick = { onLongClick = {
dialogMessage.value = String.format( confirmationDialogMessage.value = String.format(
ctx.getString(R.string.contact_delete_question), ctx.getString(R.string.contact_delete_question),
contact.name() contact.name()
) )
secondText.value = ""
lastText.value = ctx.getString(R.string.delete)
lastAction.value = { lastAction.value = {
ctx.contentResolver.delete( ctx.contentResolver.delete(
ContactsContract.RawContacts.CONTENT_URI, ContactsContract.RawContacts.CONTENT_URI,
@@ -823,7 +687,7 @@ private fun ContactsContent(
null null
) )
} }
showDialog.value = true showConfirmationDialog.value = true
} }
) )
) )
@@ -285,9 +285,9 @@ class MainActivity : ComponentActivity() {
audioScreenRoute(navController) audioScreenRoute(navController)
accountScreenRoute(navController) accountScreenRoute(navController)
codecsScreenRoute(navController) codecsScreenRoute(navController)
contactsScreenRoute(navController, viewModel) contactsScreenRoute(navController)
baresipContactScreenRoute(navController) baresipContactScreenRoute(navController)
androidContactScreenRoute(navController, viewModel) contactScreenRoute(navController, viewModel)
callsScreenRoute(navController, viewModel) callsScreenRoute(navController, viewModel)
callDetailsScreenRoute(navController, viewModel) callDetailsScreenRoute(navController, viewModel)
blockedScreenRoute(navController) blockedScreenRoute(navController)