Added possibility to save recording and delete call from history
This commit is contained in:
@ -1116,7 +1116,14 @@ class BaresipService: Service() {
|
||||
delay(500)
|
||||
val rxFile = File(call.dumpfiles[0])
|
||||
val txFile = File(call.dumpfiles[1])
|
||||
val mergedFile = File(filesPath, "${rxFile.nameWithoutExtension}_stereo.wav")
|
||||
val mergedFileName = rxFile.name
|
||||
.replace("dump", "rec")
|
||||
.replace("=>", "-")
|
||||
.replace("sip:", "")
|
||||
.replace("-enc", "")
|
||||
.replace("*", "#")
|
||||
.replace(";user=phone", "")
|
||||
val mergedFile = File(filesPath, mergedFileName)
|
||||
if (Utils.mergeWavFiles(rxFile, txFile, mergedFile)) {
|
||||
Log.d(TAG, "Automatic merge succeeded.")
|
||||
history.recording = arrayOf(mergedFile.absolutePath, "")
|
||||
|
||||
@ -5,8 +5,12 @@ import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.text.format.DateUtils
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -50,6 +54,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@ -84,7 +90,7 @@ fun NavGraphBuilder.callDetailsScreenRoute(navController: NavController, viewMod
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CallDetailsScreen(navController: NavController, callRow: CallRow) {
|
||||
|
||||
val detailsState = remember { callRow.details.toMutableStateList() }
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@ -122,13 +128,31 @@ private fun CallDetailsScreen(navController: NavController, callRow: CallRow) {
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
CallDetailsContent(LocalContext.current, contentPadding, callRow)
|
||||
CallDetailsContent(
|
||||
LocalContext.current,
|
||||
contentPadding,
|
||||
callRow,
|
||||
detailsState,
|
||||
onDelete = { detail ->
|
||||
detailsState.remove(detail)
|
||||
// If the list becomes empty, you might want to pop back
|
||||
if (detailsState.isEmpty()) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallDetailsContent(ctx: Context, contentPadding: PaddingValues, callRow: CallRow) {
|
||||
private fun CallDetailsContent(
|
||||
ctx: Context,
|
||||
contentPadding: PaddingValues,
|
||||
callRow: CallRow,
|
||||
details: SnapshotStateList<Details>,
|
||||
onDelete: (Details) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@ -137,7 +161,7 @@ private fun CallDetailsContent(ctx: Context, contentPadding: PaddingValues, call
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Peer(ctx, callRow)
|
||||
Details(ctx, callRow.details)
|
||||
Details(ctx, details, onDelete)
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,7 +179,7 @@ private fun Peer(ctx: Context, callRow: CallRow) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Details(ctx: Context, details: ArrayList<Details>) {
|
||||
private fun Details(ctx: Context, details: SnapshotStateList<Details>, onDelete: (Details) -> Unit) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = stringResource(R.string.direction),
|
||||
fontSize = 16.sp,
|
||||
@ -216,7 +240,7 @@ private fun Details(ctx: Context, details: ArrayList<Details>) {
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(78.dp))
|
||||
val durationText = startTime(detail)
|
||||
val durationText = startTime(detail, onDelete)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Duration(ctx, detail, durationText)
|
||||
}
|
||||
@ -225,7 +249,8 @@ private fun Details(ctx: Context, details: ArrayList<Details>) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun startTime(detail: Details): String {
|
||||
|
||||
private fun startTime(detail: Details, onDelete: (Details) -> Unit): String {
|
||||
val startTime = detail.startTime
|
||||
val stopTime = detail.stopTime
|
||||
val startTimeText: String
|
||||
@ -257,32 +282,111 @@ private fun startTime(detail: Details): String {
|
||||
durationText = DateUtils.formatElapsedTime(duration)
|
||||
}
|
||||
}
|
||||
Text(text = startTimeText)
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
|
||||
CustomElements.AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = stringResource(R.string.delete_call_alert),
|
||||
positiveButtonText = stringResource(R.string.delete),
|
||||
onPositiveClicked = {
|
||||
CallHistoryNew.remove(detail.startTime, detail.stopTime)
|
||||
onDelete(detail)
|
||||
},
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = startTimeText,
|
||||
modifier = Modifier.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
positiveAction.value = {
|
||||
CallHistoryNew.remove(startTime, stopTime)
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return durationText
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val showPlaybackDialog = remember { mutableStateOf(false) }
|
||||
val showDownloadDialog = remember { mutableStateOf(false) }
|
||||
|
||||
// NOTE: If detail.recording is modified elsewhere, this reference sees the change
|
||||
// because Array is mutable, but Compose won't trigger a redraw.
|
||||
val recording = detail.recording
|
||||
val mediaPlayer = remember { MediaPlayer() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 1. Setup the File Saver Launcher
|
||||
val saveLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("audio/x-wav")
|
||||
) { uri ->
|
||||
uri?.let { destinationUri ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val sourceFile = File(recording[0])
|
||||
if (sourceFile.exists()) {
|
||||
ctx.contentResolver.openOutputStream(destinationUri)?.use { output ->
|
||||
FileInputStream(sourceFile).use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(ctx, ctx.getString(R.string.recording_saved),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(ctx, "Source file not found",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save file: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(ctx, "Save failed", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackDialog(
|
||||
showDialog = showDialog,
|
||||
showDialog = showPlaybackDialog,
|
||||
mediaPlayer = mediaPlayer,
|
||||
onStop = {
|
||||
if (mediaPlayer.isPlaying) {
|
||||
mediaPlayer.stop()
|
||||
}
|
||||
mediaPlayer.reset()
|
||||
showDialog.value = false
|
||||
showPlaybackDialog.value = false
|
||||
}
|
||||
)
|
||||
|
||||
// 2. Download Confirmation Dialog
|
||||
if (showDownloadDialog.value) {
|
||||
CustomElements.AlertDialog(
|
||||
showDialog = showDownloadDialog,
|
||||
title = stringResource(R.string.save_recording),
|
||||
message = stringResource(R.string.save_recording_question),
|
||||
positiveButtonText = stringResource(R.string.save),
|
||||
onPositiveClicked = {
|
||||
showDownloadDialog.value = false
|
||||
val suggestedName = File(recording[0]).name
|
||||
saveLauncher.launch(suggestedName)
|
||||
},
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
val hasRecording = recording[0] != ""
|
||||
if (hasRecording) {
|
||||
Text(
|
||||
@ -290,111 +394,114 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.clickable(onClick = {
|
||||
if (!mediaPlayer.isPlaying) {
|
||||
mediaPlayer.reset()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
var finalFile: File? = null
|
||||
// RE-EVALUATE STATE INSIDE THE CLICK LISTENER
|
||||
val currentIsRaw = recording[0] != "" && recording[1] != ""
|
||||
val currentIsMerged = recording[0] != "" && recording[1] == ""
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
showDownloadDialog.value = true
|
||||
},
|
||||
onClick = {
|
||||
if (!mediaPlayer.isPlaying) {
|
||||
mediaPlayer.reset()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
var finalFile: File? = null
|
||||
// RE-EVALUATE STATE INSIDE THE CLICK LISTENER
|
||||
val currentIsRaw = recording[0] != "" && recording[1] != ""
|
||||
val currentIsMerged = recording[0] != "" && recording[1] == ""
|
||||
|
||||
if (currentIsRaw) {
|
||||
val fileIn = File(recording[0])
|
||||
val fileOut = File(recording[1])
|
||||
// SAFETY CHECK: If the raw file is gone, the background service
|
||||
// likely finished merging just now.
|
||||
if (!fileIn.exists()) {
|
||||
// Try to find the merged file based on naming convention as fallback
|
||||
val expectedMergedName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
|
||||
val fallbackMerged = File(BaresipService.filesPath + "/recordings", expectedMergedName)
|
||||
if (fallbackMerged.exists()) {
|
||||
Log.d(TAG, "Raw file missing, found merged fallback: ${fallbackMerged.name}")
|
||||
finalFile = fallbackMerged
|
||||
// Update state to match reality
|
||||
recording[0] = fallbackMerged.absolutePath
|
||||
recording[1] = ""
|
||||
if (currentIsRaw) {
|
||||
val fileIn = File(recording[0])
|
||||
val fileOut = File(recording[1])
|
||||
// SAFETY CHECK: If the raw file is gone, the background service
|
||||
// likely finished merging just now.
|
||||
if (!fileIn.exists()) {
|
||||
// Try to find the merged file based on naming convention as fallback
|
||||
val expectedMergedName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
|
||||
val fallbackMerged = File(BaresipService.filesPath + "/recordings", expectedMergedName)
|
||||
if (fallbackMerged.exists()) {
|
||||
Log.d(TAG, "Raw file missing, found merged fallback: ${fallbackMerged.name}")
|
||||
finalFile = fallbackMerged
|
||||
// Update state to match reality
|
||||
recording[0] = fallbackMerged.absolutePath
|
||||
recording[1] = ""
|
||||
} else {
|
||||
Log.e(TAG, "Raw file missing and fallback not found: ${recording[0]}")
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Raw file missing and fallback not found: ${recording[0]}")
|
||||
}
|
||||
} else {
|
||||
// Normal Raw processing
|
||||
val mergedFileName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
|
||||
val mergedFile = File(BaresipService.filesPath + "/recordings", mergedFileName)
|
||||
|
||||
if (mergedFile.exists()) {
|
||||
Log.d(TAG, "Using already merged file: ${mergedFile.name}")
|
||||
finalFile = mergedFile
|
||||
} else {
|
||||
if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) {
|
||||
// Normal Raw processing
|
||||
val mergedFileName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
|
||||
val mergedFile = File(BaresipService.filesPath + "/recordings", mergedFileName)
|
||||
if (mergedFile.exists()) {
|
||||
Log.d(TAG, "Using already merged file: ${mergedFile.name}")
|
||||
finalFile = mergedFile
|
||||
} else {
|
||||
if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) {
|
||||
finalFile = mergedFile
|
||||
}
|
||||
}
|
||||
|
||||
// If merge successful, update state and delete originals
|
||||
if (finalFile != null && finalFile.exists()) {
|
||||
recording[0] = finalFile.absolutePath
|
||||
recording[1] = ""
|
||||
try {
|
||||
if (fileIn.exists()) fileIn.delete()
|
||||
if (fileOut.exists()) fileOut.delete()
|
||||
CallHistoryNew.save()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "MergeWav: Failed to delete original files: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (currentIsMerged) {
|
||||
val f = File(recording[0])
|
||||
if (f.exists()) {
|
||||
Log.d(TAG, "Using already merged file: ${recording[0]}")
|
||||
finalFile = f
|
||||
} else {
|
||||
Log.e(TAG, "Merged file record exists but file is missing: ${recording[0]}")
|
||||
}
|
||||
}
|
||||
|
||||
// If merge successful, update state and delete originals
|
||||
withContext(Dispatchers.Main) {
|
||||
if (finalFile != null && finalFile.exists()) {
|
||||
recording[0] = finalFile.absolutePath
|
||||
recording[1] = ""
|
||||
try {
|
||||
if (fileIn.exists()) fileIn.delete()
|
||||
if (fileOut.exists()) fileOut.delete()
|
||||
CallHistoryNew.save()
|
||||
mediaPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
val fis = FileInputStream(finalFile)
|
||||
setDataSource(fis.fd)
|
||||
fis.close()
|
||||
setOnPreparedListener {
|
||||
it.start()
|
||||
showPlaybackDialog.value = true
|
||||
}
|
||||
setOnCompletionListener {
|
||||
showPlaybackDialog.value = false
|
||||
it.reset()
|
||||
}
|
||||
prepareAsync()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "MergeWav: Failed to delete original files: ${e.message}")
|
||||
Log.e(TAG, "Playback failed: $e")
|
||||
Toast.makeText(ctx, "Playback error",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(ctx, "Failed to process audio file",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else if (currentIsMerged) {
|
||||
// We are in merged state
|
||||
val f = File(recording[0])
|
||||
if (f.exists()) {
|
||||
Log.d(TAG, "Using already merged file: ${recording[0]}")
|
||||
finalFile = f
|
||||
} else {
|
||||
Log.e(TAG, "Merged file record exists but file is missing: ${recording[0]}")
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (finalFile != null && finalFile.exists()) {
|
||||
try {
|
||||
mediaPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
val fis = FileInputStream(finalFile)
|
||||
setDataSource(fis.fd)
|
||||
fis.close()
|
||||
setOnPreparedListener {
|
||||
it.start()
|
||||
Log.d(TAG, "Started playback")
|
||||
showDialog.value = true
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Playback complete")
|
||||
showDialog.value = false
|
||||
it.reset()
|
||||
}
|
||||
prepareAsync()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Playback failed: $e")
|
||||
Toast.makeText(ctx, "Playback error", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(ctx, "Failed to process audio file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mediaPlayer.stop()
|
||||
mediaPlayer.reset()
|
||||
showPlaybackDialog.value = false
|
||||
}
|
||||
} else {
|
||||
mediaPlayer.stop()
|
||||
mediaPlayer.reset()
|
||||
showDialog.value = false
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Text(text = durationText, modifier = Modifier.padding(end = 12.dp))
|
||||
@ -410,8 +517,6 @@ private fun PlaybackDialog(
|
||||
if (showDialog.value) {
|
||||
// State to hold progress (0.0f to 1.0f)
|
||||
var currentProgress by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
// Formatted time strings
|
||||
var currentPositionText by remember { mutableStateOf("00:00") }
|
||||
var totalDurationText by remember { mutableStateOf("00:00") }
|
||||
|
||||
@ -445,9 +550,7 @@ private fun PlaybackDialog(
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = { currentProgress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(8.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
@ -461,9 +564,7 @@ private fun PlaybackDialog(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onStop()
|
||||
}
|
||||
onClick = { onStop() }
|
||||
) {
|
||||
Text(stringResource(R.string.stop))
|
||||
}
|
||||
|
||||
@ -50,6 +50,18 @@ class CallHistoryNew(val aor: String, val peerUri: String, val direction: String
|
||||
save()
|
||||
}
|
||||
|
||||
fun remove(startTime: GregorianCalendar?, stopTime: GregorianCalendar) {
|
||||
val iterator = BaresipService.callHistory.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val h = iterator.next()
|
||||
if (h.startTime == startTime && h.stopTime == stopTime) {
|
||||
deleteRecordingFiles(h.recording)
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
fun save() {
|
||||
Log.d(TAG, "Saving history of ${BaresipService.callHistory.size} calls")
|
||||
val file = File(BaresipService.filesPath + "/call_history")
|
||||
|
||||
@ -254,8 +254,6 @@
|
||||
<string name="decrypt_password">Palauta salasanalla</string>
|
||||
<string name="delete_account">Haluatko poistaa tilin \'%1$s\'\?</string>
|
||||
<!-- Baresip Service -->
|
||||
<string name="reply">Vastaa</string>
|
||||
<string name="save">Talleta</string>
|
||||
<string name="is_calling">soittaa</string>
|
||||
<string name="missed_call_from">Vastaamaton puhelu soittajalta</string>
|
||||
<string name="missed_calls">Vastaamattomia puheluita</string>
|
||||
@ -271,8 +269,6 @@
|
||||
<string name="peer">Kumppani</string>
|
||||
<string name="time">Aika</string>
|
||||
<string name="calls_duration">Kesto</string>
|
||||
<string name="playing_recording">Tallenteen kuuntelu …</string>
|
||||
<string name="stop">Lopeta</string>
|
||||
<string name="calls_add_delete_question">Haluatko luoda uuden yhteystiedon \'%1$s\' tai poistaa
|
||||
%2$s puheluhistoriasta\?
|
||||
</string>
|
||||
@ -284,6 +280,12 @@
|
||||
<string name="call_answered_elsewhere">Puheluun vastattu muualla</string>
|
||||
<string name="call_missed">Puheluun ei vastattu</string>
|
||||
<string name="call_rejected">Puhelu hylätty</string>
|
||||
<!-- Call Details Activity -->
|
||||
<string name="playing_recording">Tallenteen kuuntelu …</string>
|
||||
<string name="save_recording">Talleta tallenne</string>
|
||||
<string name="save_recording_question">Haluatko tallentaa tämän tallenteen?</string>
|
||||
<string name="recording_saved">Tallenne tallennettu</string>
|
||||
<string name="delete_call_alert">Haluatko poistaa tämän puhelun historiata?</string>
|
||||
<!-- Chat Activity -->
|
||||
<string name="chat_with">Viestiketju %1$s</string>
|
||||
<string name="new_message">Uusi viesti</string>
|
||||
@ -460,6 +462,9 @@
|
||||
<string name="info">Tieto</string>
|
||||
<string name="notice">Huomio</string>
|
||||
<string name="cancel">Peruuta</string>
|
||||
<string name="stop">Lopeta</string>
|
||||
<string name="reply">Vastaa</string>
|
||||
<string name="save">Talleta</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="yes">Kyllä</string>
|
||||
<string name="no">Ei</string>
|
||||
|
||||
@ -242,8 +242,6 @@
|
||||
<string name="decrypt_password">Decrypt Password</string>
|
||||
<string name="delete_account">Do you want to delete account \'%1$s\'\?</string>
|
||||
<!-- Baresip Service -->
|
||||
<string name="reply">Reply</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="is_calling">is calling</string>
|
||||
<string name="missed_call_from">Missed call from</string>
|
||||
<string name="missed_calls">Missed calls</string>
|
||||
@ -260,8 +258,6 @@
|
||||
<string name="direction">Direction</string>
|
||||
<string name="time">Time</string>
|
||||
<string name="calls_duration">Duration</string>
|
||||
<string name="playing_recording">Playing recording …</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="calls_add_delete_question">Do you want to add \'%1$s\' to contacts or delete
|
||||
%2$s from call history\?</string>
|
||||
<string name="calls_delete_question">Do you want to delete \'%1$s\' %2$s from call history\?</string>
|
||||
@ -272,6 +268,12 @@
|
||||
<string name="call_answered_elsewhere">Call answered elsewhere</string>
|
||||
<string name="call_missed">Call missed</string>
|
||||
<string name="call_rejected">Call rejected</string>
|
||||
<!-- Call Details Activity -->
|
||||
<string name="playing_recording">Playing recording …</string>
|
||||
<string name="save_recording">Save Recording</string>
|
||||
<string name="save_recording_question">Do you want to save this recording?</string>
|
||||
<string name="recording_saved">Recording saved</string>
|
||||
<string name="delete_call_alert">Do you want to delete this call from history?</string>
|
||||
<!-- Chat Activity -->
|
||||
<string name="chat_with">Chat with %1$s</string>
|
||||
<string name="new_message">New message</string>
|
||||
@ -438,6 +440,9 @@
|
||||
<string name="info">Info</string>
|
||||
<string name="notice">Notice</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="reply">Reply</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
|
||||
Reference in New Issue
Block a user