Migrated app to compose navigation
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.core.net.toUri
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.tutpro.baresip.CallRow.Details
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
fun NavGraphBuilder.callDetailsScreenRoute(navController: NavController, viewModel: ViewModel) {
|
||||
composable("call_details") { backStackEntry ->
|
||||
val callRow = remember { viewModel.consumeSelectedCallRow() }
|
||||
CallDetailsScreen(navController, callRow!!)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CallDetailsScreen(navController: NavController, callRow: CallRow) {
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize().imePadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.call_details),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navController::popBackStack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
CallDetailsContent(LocalContext.current, contentPadding, callRow)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallDetailsContent(ctx: Context, contentPadding: PaddingValues, callRow: CallRow) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.padding(top = 16.dp, start = 16.dp, end = 4.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Peer(ctx, callRow)
|
||||
Details(ctx, callRow.details)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Peer(ctx: Context, callRow: CallRow) {
|
||||
val account = Account.ofAor(callRow.aor)!!
|
||||
val headerText = stringResource(R.string.peer) + " " + Utils.friendlyUri(ctx, callRow.peerUri, account)
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Details(ctx: Context, details: ArrayList<Details>) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = stringResource(R.string.direction),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.width(96.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(text = stringResource(R.string.time),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = stringResource(R.string.calls_duration),
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(details) { detail ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(detail.direction),
|
||||
contentDescription = "Direction",
|
||||
modifier = Modifier.width(64.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(38.dp))
|
||||
val durationText = startTime(detail)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Duration(ctx, detail, durationText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun startTime(detail: Details): String {
|
||||
val startTime = detail.startTime
|
||||
val stopTime = detail.stopTime
|
||||
val startTimeText: String
|
||||
val durationText: String
|
||||
val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) {
|
||||
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
stringResource(R.string.today) + " " + fmt.format(stopTime.time)
|
||||
} else {
|
||||
val fmt = DateFormat.getDateTimeInstance()
|
||||
fmt.format(stopTime.time)
|
||||
}
|
||||
if (startTime == GregorianCalendar(0, 0, 0)) {
|
||||
startTimeText = stopText
|
||||
durationText = "?"
|
||||
} else {
|
||||
if (startTime == null) {
|
||||
startTimeText = stopText
|
||||
durationText = ""
|
||||
} else {
|
||||
val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
|
||||
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
stringResource(R.string.today) + " " + fmt.format(startTime.time)
|
||||
} else {
|
||||
val fmt = DateFormat.getDateTimeInstance()
|
||||
fmt.format(startTime.time)
|
||||
}
|
||||
startTimeText = startText
|
||||
val duration = (stopTime.time.time - startTime.time.time) / 1000
|
||||
durationText = DateUtils.formatElapsedTime(duration)
|
||||
}
|
||||
}
|
||||
Text(text = startTimeText, color = LocalCustomColors.current.itemText)
|
||||
return durationText
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val recording = detail.recording
|
||||
val decPlayer = MediaPlayer()
|
||||
val encPlayer = MediaPlayer()
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.playing_recording),
|
||||
message = "",
|
||||
)
|
||||
|
||||
if (recording[0] != "") {
|
||||
Text(
|
||||
text = durationText,
|
||||
color = LocalCustomColors.current.accent,
|
||||
modifier = Modifier.padding(end = 12.dp)
|
||||
.clickable(onClick = {
|
||||
if (!decPlayer.isPlaying && !encPlayer.isPlaying) {
|
||||
decPlayer.reset()
|
||||
encPlayer.reset()
|
||||
Log.d(TAG, "Playing recordings ${recording[0]} and ${recording[1]}")
|
||||
decPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
encPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
it.start()
|
||||
decPlayer.start()
|
||||
Log.d(TAG, "Started players")
|
||||
showDialog.value = true
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping encPlayer")
|
||||
it.stop()
|
||||
showDialog.value = false
|
||||
}
|
||||
try {
|
||||
val file = recording[0]
|
||||
val encFile = File(file)
|
||||
.copyTo(File(BaresipService.filesPath +
|
||||
"/tmp/encode.wav"), true)
|
||||
val encUri = encFile.toUri()
|
||||
setDataSource(ctx, encUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "encPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "encPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "encPlayer Exception: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping decPlayer")
|
||||
it.stop()
|
||||
showDialog.value = false
|
||||
}
|
||||
try {
|
||||
val file = recording[1]
|
||||
val decFile = File(file)
|
||||
.copyTo(File(BaresipService.filesPath +
|
||||
"/tmp/decode.wav"), true)
|
||||
val decUri = decFile.toUri()
|
||||
setDataSource(ctx, decUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "decPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "decPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "decPlayer Exception: $e")
|
||||
}
|
||||
}
|
||||
} else if (decPlayer.isPlaying && encPlayer.isPlaying) {
|
||||
decPlayer.stop()
|
||||
encPlayer.stop()
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
Text(text = durationText,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
color = LocalCustomColors.current.itemText)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user