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 ) } } } } }