Files
Juha Heinanen caa1baf0c8 Show alert if contact does not have suitable URI
Show alert if calling or messaging is tried on mobile account when device is in airplane mode
2026-05-19 09:16:56 +03:00

353 lines
13 KiB
Kotlin

package com.tutpro.baresip
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Chat
import androidx.compose.material.icons.outlined.Call
import androidx.compose.material.icons.outlined.Email
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import coil.compose.AsyncImage
import androidx.core.net.toUri
fun NavGraphBuilder.contactScreenRoute(navController: NavController, viewModel: ViewModel) {
composable(
route = "contact/{name}",
arguments = listOf(navArgument("name") { type = NavType.StringType })
) { backStackEntry ->
val ctx = LocalContext.current
val name = backStackEntry.arguments?.getString("name")!!
ContactScreen(
ctx = ctx,
viewModel = viewModel,
navController = navController,
name = name
)
}
}
@Composable
private fun ContactScreen(ctx: Context, viewModel: ViewModel, navController: NavController, name: String) {
val contact = Contact.baresipContact(name) ?: Contact.androidContact(name)
if (contact == null) {
Log.e(TAG, "No contact found with name $name")
navController.navigateUp()
return
}
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
containerColor = MaterialTheme.colorScheme.background,
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
) {
TopAppBar(name, navController)
}
},
content = { contentPadding ->
ContactContent(ctx, viewModel, navController, contentPadding, contact)
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopAppBar(title: String, navController: NavController) {
TopAppBar(
title = { Text(text = title, fontWeight = FontWeight.Bold) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
),
windowInsets = WindowInsets(0, 0, 0, 0),
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
}
}
)
}
@Composable
private fun ContactContent(
ctx: Context,
viewModel: ViewModel,
navController: NavController,
contentPadding: PaddingValues,
contact: Contact
) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(contentPadding)
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Avatar(contact)
ContactName(contact.name())
Uris(ctx, viewModel, navController, contact)
if (contact.email().isNotEmpty())
Email(ctx, contact.email())
}
}
@Composable
private fun TextAvatar(text: String, color: Int) {
Box(
modifier = Modifier.size(avatarSize.dp),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(SolidColor(Color(color)))
}
Text(text, fontSize = 72.sp, color = Color.White)
}
}
@Composable
private fun ImageAvatar(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(
model = uri,
contentDescription = stringResource(R.string.avatar_image),
contentScale = ContentScale.Crop,
modifier = Modifier.size(avatarSize.dp).clip(CircleShape)
)
}
@Composable
private fun Avatar(contact: Contact) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
val color = contact.colorInt()
val name = contact.name()
when (contact) {
is Contact.BaresipContact -> {
if (contact.avatarImage != null)
ImageAvatar(contact.avatarImage!!)
else
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
private fun ContactName(name: String) {
Row(
Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp),
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
) {
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
)
}
}
}
@Composable
private fun Uris(
ctx: Context,
viewModel: ViewModel,
navController: NavController,
contact: Contact
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
for (uri in contact.uris()) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(R.string.bullet_item, uri.substringAfter(":")),
modifier = Modifier.weight(1f),
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground,
)
// Chat Button
IconButton(
onClick = {
val aor = viewModel.selectedAor.value
val ua = UserAgent.ofAor(aor)
if (ua == null)
Log.w(TAG, "Message clickable did not find AoR $aor")
else {
if (ua.account.isMobile && Utils.isAirplaneModeOn(ctx)) {
handleDialog(ctx, ctx.getString(R.string.notice),
ctx.getString(R.string.no_airplane_mode))
} 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)
intent.putExtra("uap", ua.uap)
intent.putExtra("peer", uri)
handleIntent(ctx, viewModel, intent, "message")
navController.navigate("main") {
popUpTo("main") { inclusive = false }
launchSingleTop = true
}
}
}
}
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Chat,
contentDescription = "Send Message",
tint = MaterialTheme.colorScheme.onBackground
)
}
// Call Button
IconButton(
onClick = {
val aor = viewModel.selectedAor.value
val ua = UserAgent.ofAor(aor)
if (ua == null)
Log.w(TAG, "Call clickable did not find AoR $aor")
else {
if (ua.account.isMobile && Utils.isAirplaneModeOn(ctx)) {
handleDialog(ctx, ctx.getString(R.string.notice),
ctx.getString(R.string.no_airplane_mode))
} else {
val intent = Intent(ctx, MainActivity::class.java)
intent.putExtra("uap", ua.uap)
intent.putExtra("peer", uri)
handleIntent(ctx, viewModel, intent, "call")
navController.navigate("main") {
popUpTo("main") { inclusive = false }
launchSingleTop = true
}
}
}
}
) {
Icon(
imageVector = Icons.Outlined.Call,
contentDescription = "Call",
tint = MaterialTheme.colorScheme.onBackground
)
}
}
}
}
}