Migrated app to compose navigation
This commit is contained in:
@ -76,108 +76,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/about_title"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AccountsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/accounts"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AccountActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/account" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".CodecsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/codecs"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".AccountActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ContactsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/contacts"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".BaresipContactActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/contact"
|
||||
android:parentActivityName=".ContactsActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AndroidContactActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/contact"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".ContactsActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ConfigActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/configuration"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AudioActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/audio_settings"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".ConfigActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".CallsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/call_history"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".CallDetailsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/call_details"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".CallsActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ChatsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/chats"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ChatActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/chat" >
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".BaresipService"
|
||||
android:enabled="true"
|
||||
|
||||
@ -411,8 +411,10 @@ static int runLoggingThread()
|
||||
dup2(pfd[1], 2);
|
||||
|
||||
int ret = pthread_create(&loggingThread, NULL, loggingFunction, NULL);
|
||||
if (ret != 0)
|
||||
if (ret != 0) {
|
||||
LOGE("failed to create logging thread: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
pthread_detach(loggingThread);
|
||||
|
||||
@ -456,8 +458,10 @@ JNIEXPORT void JNICALL Java_com_tutpro_baresip_BaresipService_baresipStart(
|
||||
runLoggingThread();
|
||||
|
||||
err = libre_init();
|
||||
if (err)
|
||||
if (err) {
|
||||
LOGE("failed to init libre");
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (re_thread_check(true) == 0) {
|
||||
LOGI("attaching to re thread\n");
|
||||
|
||||
@ -1,151 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
class AboutActivity : ComponentActivity() {
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
Utils.addActivity("about")
|
||||
|
||||
val aboutTitle = getString(R.string.about_title)
|
||||
val aboutText = String.format(getString(R.string.about_text),
|
||||
BuildConfig.VERSION_NAME)
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
AboutContent(aboutTitle, aboutText) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AboutContent(title: String, text: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Localized description",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
) { contentPadding ->
|
||||
Text(
|
||||
text = AnnotatedString.Companion.fromHtml(
|
||||
htmlString = text,
|
||||
linkStyles = TextLinkStyles(
|
||||
style = SpanStyle(color = LocalCustomColors.current.accent)
|
||||
)
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
.background(LocalCustomColors.current.background)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("about")
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
96
app/src/main/kotlin/com/tutpro/baresip/AboutScreen.kt
Normal file
96
app/src/main/kotlin/com/tutpro/baresip/AboutScreen.kt
Normal file
@ -0,0 +1,96 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
fun NavGraphBuilder.aboutScreenRoute(navController: NavController) {
|
||||
composable("about") {
|
||||
AboutScreen(onBack = { navController.popBackStack() })
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AboutScreen(onBack: () -> Unit) {
|
||||
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.about_title),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { contentPadding ->
|
||||
Text(
|
||||
text = AnnotatedString.fromHtml(
|
||||
htmlString = stringResource(R.string.about_text, BuildConfig.VERSION_NAME),
|
||||
linkStyles = TextLinkStyles(SpanStyle(color = LocalCustomColors.current.accent))
|
||||
),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,6 +249,14 @@ class Account(val accp: Long) {
|
||||
return res
|
||||
}
|
||||
|
||||
fun saveAccounts() {
|
||||
var accounts = ""
|
||||
for (a in accounts()) accounts = accounts + a.print() + "\n"
|
||||
Utils.putFileContents(BaresipService.filesPath + "/accounts",
|
||||
accounts.toByteArray(Charsets.UTF_8))
|
||||
// Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
|
||||
}
|
||||
|
||||
fun ofAor(aor: String): Account? {
|
||||
for (ua in BaresipService.uas.value)
|
||||
if (ua.account.aor == aor) return ua.account
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1885
app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt
Normal file
1885
app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,403 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.Image
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
class AccountsActivity : ComponentActivity() {
|
||||
|
||||
internal lateinit var aor: String
|
||||
private lateinit var mediaEncMap: Map<String, String>
|
||||
private lateinit var mediaNatMap: Map<String, String>
|
||||
|
||||
private var showAccounts = mutableStateOf(true)
|
||||
private var lastClick: Long = 0
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
val title = getString(R.string.accounts)
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
Utils.addActivity("accounts,$aor")
|
||||
|
||||
mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF",
|
||||
"srtp-mand" to "SRTP-MAND", "srtp" to "SRTP", "" to "--")
|
||||
|
||||
mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
AccountsScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = { NewAccount(ctx) },
|
||||
content = { contentPadding ->
|
||||
AccountsContent(ctx, contentPadding)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val message = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = message.value,
|
||||
positiveButtonText = stringResource(R.string.delete),
|
||||
onPositiveClicked = positiveAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel),
|
||||
)
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
if (showAccounts.value && BaresipService.uas.value.isNotEmpty()) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
|
||||
.verticalScrollbar(scrollState)
|
||||
.verticalScroll(state = scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
for (ua in BaresipService.uas.value) {
|
||||
val account = ua.account
|
||||
val aor = account.aor
|
||||
val text = if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
account.aor.substringAfter(":")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 20.sp,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier.weight(1f).padding(start = 10.dp)
|
||||
.clickable {
|
||||
val i = Intent(ctx, AccountActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
i.putExtras(b)
|
||||
startActivity(i)
|
||||
}
|
||||
)
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
message.value = String.format(
|
||||
ctx.getString(R.string.delete_account),
|
||||
text
|
||||
)
|
||||
positiveAction.value = {
|
||||
CallHistoryNew.clear(aor)
|
||||
Message.clearMessagesOfAor(aor)
|
||||
ua.remove()
|
||||
Api.ua_destroy(ua.uap)
|
||||
saveAccounts()
|
||||
showAccounts.value = false
|
||||
showAccounts.value = true
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
},
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
contentColor = LocalCustomColors.current.secondary
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewAccount(ctx: Context) {
|
||||
var newAor by remember { mutableStateOf("") }
|
||||
val focusManager = LocalFocusManager.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newAor,
|
||||
placeholder = { Text(text = stringResource(R.string.new_account)) },
|
||||
onValueChange = { newAor = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.new_account)
|
||||
alertMessage.value = getString(R.string.accounts_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
singleLine = false,
|
||||
trailingIcon = {
|
||||
if (newAor.isNotEmpty()) {
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.clickable { newAor = "" }
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { LabelText(stringResource(R.string.new_account)) },
|
||||
textStyle = TextStyle(fontSize = 18.sp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.plus),
|
||||
contentDescription = "Add",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(48.dp)
|
||||
.clickable(
|
||||
onClick = {
|
||||
val account = createNew(newAor.trim())
|
||||
if (account != null) {
|
||||
val i = Intent(ctx, AccountActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", account.aor)
|
||||
b.putString("kind", "new")
|
||||
i.putExtras(b)
|
||||
startActivity(i)
|
||||
newAor = ""
|
||||
focusManager.clearFocus()
|
||||
showAccounts.value = false
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNew(newAor: String): Account? {
|
||||
|
||||
val aor = if (newAor.startsWith("sip:"))
|
||||
newAor
|
||||
else
|
||||
"sip:$newAor"
|
||||
|
||||
if (!Utils.checkAor(aor)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = String.format(getString(R.string.invalid_aor), aor.split(":")[1])
|
||||
showAlert.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
if (Account.ofAor(aor) != null) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = String.format(getString(R.string.account_exists), aor.split(":")[1])
|
||||
showAlert.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
val ua = UserAgent.uaAlloc(
|
||||
"<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;mwi=no"
|
||||
)
|
||||
if (ua == null) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = getString(R.string.account_allocation_failure)
|
||||
showAlert.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
// Api.account_debug(ua.account.accp)
|
||||
val acc = ua.account
|
||||
Log.d(TAG, "Allocated UA ${ua.uap} with SIP URI ${acc.luri}")
|
||||
saveAccounts()
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("accounts,$aor")
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
MainActivity.activityAor = aor
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
showAccounts.value = true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun saveAccounts() {
|
||||
var accounts = ""
|
||||
for (a in Account.accounts()) accounts = accounts + a.print() + "\n"
|
||||
Utils.putFileContents(BaresipService.filesPath + "/accounts",
|
||||
accounts.toByteArray(Charsets.UTF_8))
|
||||
// Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
306
app/src/main/kotlin/com/tutpro/baresip/AccountsScreen.kt
Normal file
306
app/src/main/kotlin/com/tutpro/baresip/AccountsScreen.kt
Normal file
@ -0,0 +1,306 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
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.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.navigationBarsPadding
|
||||
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.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
fun NavGraphBuilder.accountsScreenRoute(navController: NavController) {
|
||||
composable("accounts") {
|
||||
AccountsScreen(navController)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountsScreen(navController: NavController) {
|
||||
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.accounts),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0)
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = { NewAccount(navController) },
|
||||
content = { contentPadding ->
|
||||
AccountsContent(contentPadding, navController)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AccountsContent(contentPadding: PaddingValues, navController: NavController) {
|
||||
|
||||
val ctx = LocalContext.current
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val message = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = message.value,
|
||||
positiveButtonText = stringResource(R.string.delete),
|
||||
onPositiveClicked = positiveAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel),
|
||||
)
|
||||
|
||||
val showAccounts = remember { mutableStateOf(true) }
|
||||
|
||||
if (showAccounts.value && BaresipService.uas.value.isNotEmpty()) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
|
||||
.verticalScrollbar(scrollState)
|
||||
.verticalScroll(state = scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
for (ua in BaresipService.uas.value) {
|
||||
val account = ua.account
|
||||
val aor = account.aor
|
||||
val text = if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
account.aor.substringAfter(":")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 20.sp,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier.weight(1f).padding(start = 10.dp)
|
||||
.clickable {
|
||||
navController.navigate("account/$aor/old")
|
||||
}
|
||||
)
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
message.value = String.format(
|
||||
ctx.getString(R.string.delete_account),
|
||||
text
|
||||
)
|
||||
positiveAction.value = {
|
||||
CallHistoryNew.clear(aor)
|
||||
Message.clearMessagesOfAor(aor)
|
||||
ua.remove()
|
||||
Api.ua_destroy(ua.uap)
|
||||
Account.saveAccounts()
|
||||
showAccounts.value = false
|
||||
showAccounts.value = true
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
contentColor = LocalCustomColors.current.secondary
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewAccount(navController: NavController) {
|
||||
|
||||
val alertTitle = remember { mutableStateOf("") }
|
||||
val alertMessage = remember { mutableStateOf("") }
|
||||
val showAlert = remember { mutableStateOf(false) }
|
||||
|
||||
if (showAlert.value)
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
|
||||
fun createNew(ctx: Context, newAor: String): Account? {
|
||||
|
||||
val aor = if (newAor.startsWith("sip:"))
|
||||
newAor
|
||||
else
|
||||
"sip:$newAor"
|
||||
|
||||
if (!Utils.checkAor(aor)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value =
|
||||
String.format(ctx.getString(R.string.invalid_aor), aor.split(":")[1])
|
||||
showAlert.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
if (Account.ofAor(aor) != null) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value =
|
||||
String.format(ctx.getString(R.string.account_exists), aor.split(":")[1])
|
||||
showAlert.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
val ua = UserAgent.uaAlloc(
|
||||
"<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;mwi=no"
|
||||
)
|
||||
if (ua == null) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = ctx.getString(R.string.account_allocation_failure)
|
||||
showAlert.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
// Api.account_debug(ua.account.accp)
|
||||
val acc = ua.account
|
||||
Log.d(TAG, "Allocated UA ${ua.uap} with SIP URI ${acc.luri}")
|
||||
Account.saveAccounts()
|
||||
return acc
|
||||
} // createNew
|
||||
|
||||
var newAor by remember { mutableStateOf("") }
|
||||
val focusManager = LocalFocusManager.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
OutlinedTextField(
|
||||
value = newAor,
|
||||
placeholder = { Text(text = stringResource(R.string.new_account)) },
|
||||
onValueChange = { newAor = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.new_account)
|
||||
alertMessage.value = ctx.getString(R.string.accounts_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
singleLine = false,
|
||||
trailingIcon = {
|
||||
if (newAor.isNotEmpty()) {
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.clickable { newAor = "" }
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { LabelText(stringResource(R.string.new_account)) },
|
||||
textStyle = TextStyle(fontSize = 18.sp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.plus),
|
||||
contentDescription = "Add",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(48.dp)
|
||||
.clickable(
|
||||
onClick = {
|
||||
val account = createNew(ctx, newAor.trim())
|
||||
if (account != null) {
|
||||
navController.navigate("account/${account.aor}/new")
|
||||
newAor = ""
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,305 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.Canvas
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.Surface
|
||||
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.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
class AndroidContactActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var aor: String
|
||||
private lateinit var name: String
|
||||
|
||||
private var color = 0
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
name = intent.getStringExtra("name")!!
|
||||
|
||||
val title: String = name
|
||||
val contact = Contact.androidContact(name)
|
||||
|
||||
if (contact == null) {
|
||||
Log.e(TAG, "No Android contact found with name $name")
|
||||
goBack()
|
||||
}
|
||||
|
||||
Utils.addActivity("android contact, $name")
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ContactScreen(this, title, contact!!) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContactScreen(ctx: Context, title: String, contact: Contact.AndroidContact,
|
||||
navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxHeight().imePadding().safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title, color = LocalCustomColors.current.grayLight,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
ContactContent(ctx, contentPadding, contact)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactContent(ctx: Context, contentPadding: PaddingValues, contact: Contact.AndroidContact) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Avatar(contact)
|
||||
ContactName()
|
||||
Uris(ctx, contact)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextAvatar(text: String, size: Int, color: Int) {
|
||||
Box(
|
||||
modifier = Modifier.size(size.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(SolidColor(Color(color)))
|
||||
}
|
||||
Text(text, fontSize = 72.sp, color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageAvatar(uri: Uri, size: Int) {
|
||||
AsyncImage(
|
||||
model = uri,
|
||||
contentDescription = stringResource(R.string.avatar_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(size.dp).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Avatar(contact: Contact.AndroidContact) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
color = contact.color
|
||||
val thumbnailUri = contact.thumbnailUri
|
||||
if (thumbnailUri != null)
|
||||
ImageAvatar(thumbnailUri, 96)
|
||||
else
|
||||
TextAvatar(if (name == "") "" else name[0].toString(), 96, color)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactName() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(name, fontSize = 24.sp, color = LocalCustomColors.current.itemText)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Uris(ctx: Context, contact: Contact.AndroidContact) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(contact.uris) { uri ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = uri.substringAfter(":"),
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 18.sp,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.message),
|
||||
colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
|
||||
contentDescription = "Send Message",
|
||||
modifier = Modifier.padding(end = 24.dp).clickable {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "message")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(TAG, "message clickable did not find AoR $aor")
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.call_small),
|
||||
colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
|
||||
contentDescription = "Call",
|
||||
modifier = Modifier.clickable {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "call")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(TAG, "call clickable did not find AoR $aor")
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("android contact,$name")
|
||||
setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
272
app/src/main/kotlin/com/tutpro/baresip/AndroidContactScreen.kt
Normal file
272
app/src/main/kotlin/com/tutpro/baresip/AndroidContactScreen.kt
Normal file
@ -0,0 +1,272 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Canvas
|
||||
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.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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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.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
|
||||
|
||||
fun NavGraphBuilder.androidContactScreenRoute(navController: NavController, viewModel: ViewModel) {
|
||||
composable(
|
||||
route = "android_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.androidContact(name)
|
||||
if (contact == null) {
|
||||
Log.e(TAG, "No Android contact found with name $name")
|
||||
navController.popBackStack()
|
||||
}
|
||||
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(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,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactContent(
|
||||
ctx: Context,
|
||||
viewModel: ViewModel,
|
||||
navController: NavController,
|
||||
contentPadding: PaddingValues,
|
||||
contact: Contact.AndroidContact
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Avatar(contact)
|
||||
ContactName(contact.name)
|
||||
Uris(ctx, viewModel, navController, contact)
|
||||
}
|
||||
}
|
||||
|
||||
@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(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.AndroidContact) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
val color = contact.color
|
||||
val name = contact.name
|
||||
val thumbnailUri = contact.thumbnailUri
|
||||
if (thumbnailUri != null)
|
||||
ImageAvatar(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.Start
|
||||
) {
|
||||
Text(name, fontSize = 24.sp, color = LocalCustomColors.current.itemText)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Uris(
|
||||
ctx: Context,
|
||||
viewModel: ViewModel,
|
||||
navController: NavController,
|
||||
contact: Contact.AndroidContact
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(contact.uris) { uri ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = uri.substringAfter(":"),
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 18.sp,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.message),
|
||||
colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
|
||||
contentDescription = "Send Message",
|
||||
modifier = Modifier.padding(end = 24.dp).clickable {
|
||||
val aor = viewModel.selectedAor.value
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null)
|
||||
Log.w(TAG, "message clickable did not find AoR $aor")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.call_small),
|
||||
colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
|
||||
contentDescription = "Call",
|
||||
modifier = Modifier.clickable {
|
||||
val aor = viewModel.selectedAor.value
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null)
|
||||
Log.w(TAG, "message clickable did not find AoR $aor")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
@ -10,59 +11,61 @@ import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val darkTheme = remember { BaresipService.darkTheme }
|
||||
val useDarkTheme by remember { BaresipService.darkTheme }
|
||||
|
||||
// "normal" palette, nothing change here
|
||||
val colorScheme = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme.value) dynamicDarkColorScheme(context = LocalContext.current)
|
||||
else dynamicLightColorScheme(context = LocalContext.current)
|
||||
if (useDarkTheme)
|
||||
dynamicDarkColorScheme(context = LocalContext.current)
|
||||
else
|
||||
if (isSystemInDarkTheme())
|
||||
dynamicDarkColorScheme(context = LocalContext.current)
|
||||
else
|
||||
dynamicLightColorScheme(context = LocalContext.current)
|
||||
}
|
||||
darkTheme.value -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
useDarkTheme -> darkColorScheme()
|
||||
else ->
|
||||
if (isSystemInDarkTheme())
|
||||
darkColorScheme()
|
||||
else
|
||||
lightColorScheme()
|
||||
}
|
||||
|
||||
// logic for which custom palette to use
|
||||
val customColorsPalette =
|
||||
if (darkTheme.value) DarkCustomColors
|
||||
if (useDarkTheme) DarkCustomColors
|
||||
else LightCustomColors
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
val decorView = window.decorView
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
|
||||
// Ensure insets are applied correctly
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
// A common threshold for luminance is 0.5. Colors with luminance > 0.5 are considered light.
|
||||
val isBackgroundEffectivelyLight = ColorUtils.calculateLuminance(customColorsPalette.background.toArgb()) > 0.5
|
||||
|
||||
// Handle the status bar appearance
|
||||
val insetsController = WindowCompat.getInsetsController(window, decorView)
|
||||
window.statusBarColor = customColorsPalette.background.toArgb()
|
||||
window.navigationBarColor = customColorsPalette.background.toArgb()
|
||||
insetsController.apply {
|
||||
isAppearanceLightStatusBars = !darkTheme.value
|
||||
isAppearanceLightNavigationBars = !darkTheme.value
|
||||
}
|
||||
insetsController.isAppearanceLightStatusBars = isBackgroundEffectivelyLight
|
||||
insetsController.isAppearanceLightNavigationBars = isBackgroundEffectivelyLight
|
||||
}
|
||||
}
|
||||
|
||||
// here is the important point, where you will expose custom objects
|
||||
CompositionLocalProvider(
|
||||
LocalCustomColors provides customColorsPalette // our custom palette
|
||||
LocalCustomColors provides customColorsPalette
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme, // the MaterialTheme still uses the "normal" palette
|
||||
colorScheme = colorScheme, // MaterialTheme still uses the "normal" colorScheme
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,700 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.clickable
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.safeDrawingPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
class AudioActivity : ComponentActivity() {
|
||||
|
||||
private var save = false
|
||||
private var restart = false
|
||||
private var oldCallVolume = BaresipService.callVolume
|
||||
private var newCallVolume = oldCallVolume
|
||||
private var oldMicGain = ""
|
||||
private var newMicGain = ""
|
||||
private var newSpeakerPhone = BaresipService.speakerPhone
|
||||
private val modules = Config.variables("module")
|
||||
private var newAudioModules = mutableMapOf<String, Boolean>()
|
||||
private var oldOpusBitrate = Config.variable("opus_bitrate")
|
||||
private var newOpusBitrate = oldOpusBitrate
|
||||
private var oldOpusPacketLoss = Config.variable("opus_packet_loss")
|
||||
private var newOpusPacketLoss = oldOpusPacketLoss
|
||||
private var newAudioDelay = BaresipService.audioDelay.toString()
|
||||
private var newToneCountry = BaresipService.toneCountry
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
Utils.addActivity("audio")
|
||||
|
||||
if (!BaresipService.agcAvailable)
|
||||
oldMicGain = Config.variable("augain")
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
AudioScreen { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AudioScreen(navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(navigateBack) },
|
||||
content = { contentPadding ->
|
||||
AudioContent(contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(navigateBack: () -> Unit) {
|
||||
androidx.compose.material3.TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.audio_settings),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
checkOnClick()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AudioContent(contentPadding: PaddingValues) {
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 4.dp)
|
||||
.verticalScrollbar(scrollState)
|
||||
.verticalScroll(state = scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
CallVolume()
|
||||
MicGain()
|
||||
SpeakerPhone()
|
||||
AudioModules()
|
||||
OpusBitRate()
|
||||
OpusPacketLoss()
|
||||
AudioDelay()
|
||||
ToneCountry()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallVolume() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end=10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(text = stringResource(R.string.default_call_volume),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.default_call_volume)
|
||||
alertMessage.value = getString(R.string.default_call_volume_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
val isDropDownExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val volNames = listOf("--", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
|
||||
val volValues = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
val itemPosition = remember {
|
||||
mutableIntStateOf(volValues.indexOf(oldCallVolume))
|
||||
}
|
||||
Box {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable {
|
||||
isDropDownExpanded.value = true
|
||||
}
|
||||
) {
|
||||
Text(text = volNames[itemPosition.intValue],
|
||||
color = LocalCustomColors.current.itemText)
|
||||
CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
|
||||
tint = LocalCustomColors.current.itemText)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = isDropDownExpanded.value,
|
||||
onDismissRequest = {
|
||||
isDropDownExpanded.value = false
|
||||
}) {
|
||||
volNames.forEachIndexed { index, vol ->
|
||||
DropdownMenuItem(text = {
|
||||
Text(text = vol)
|
||||
},
|
||||
onClick = {
|
||||
isDropDownExpanded.value = false
|
||||
itemPosition.intValue = index
|
||||
newCallVolume = volValues[index]
|
||||
})
|
||||
if (index < 10)
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MicGain() {
|
||||
if (!BaresipService.agcAvailable)
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
var micGain by remember { mutableStateOf(oldMicGain) }
|
||||
newMicGain = micGain
|
||||
OutlinedTextField(
|
||||
value = micGain,
|
||||
placeholder = { Text(stringResource(R.string.microphone_gain)) },
|
||||
onValueChange = {
|
||||
micGain = it
|
||||
newMicGain = micGain
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.microphone_gain)
|
||||
alertMessage.value = getString(R.string.microphone_gain_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { LabelText(stringResource(R.string.microphone_gain)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpeakerPhone() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end=10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(text = stringResource(R.string.speaker_phone),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.speaker_phone)
|
||||
alertMessage.value = getString(R.string.speaker_phone_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) }
|
||||
Switch(
|
||||
checked = speakerPhone,
|
||||
onCheckedChange = {
|
||||
speakerPhone = it
|
||||
newSpeakerPhone = speakerPhone
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AudioModules() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(text = stringResource(R.string.audio_modules_title),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp,
|
||||
modifier = Modifier.clickable {
|
||||
alertTitle.value = getString(R.string.audio_modules_title)
|
||||
alertMessage.value = getString(R.string.audio_modules_help)
|
||||
showAlert.value = true
|
||||
})
|
||||
for (module in audioModules) {
|
||||
Row(horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 18.dp, end = 10.dp)
|
||||
) {
|
||||
Text(text = String.format(getString(R.string.bullet_item), module),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var checked by remember { mutableStateOf(modules.contains("${module}.so")) }
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
newAudioModules[module] = checked
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpusBitRate() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
var opusBitrate by remember { mutableStateOf(oldOpusBitrate) }
|
||||
newOpusBitrate = opusBitrate
|
||||
OutlinedTextField(
|
||||
value = opusBitrate,
|
||||
placeholder = { Text(stringResource(R.string.opus_bit_rate)) },
|
||||
onValueChange = {
|
||||
opusBitrate = it
|
||||
newOpusBitrate = opusBitrate
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.opus_bit_rate)
|
||||
alertMessage.value = getString(R.string.opus_bit_rate_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
|
||||
label = { LabelText(stringResource(R.string.opus_bit_rate)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpusPacketLoss() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
var opusPacketLoss by remember { mutableStateOf(oldOpusPacketLoss) }
|
||||
newOpusPacketLoss = opusPacketLoss
|
||||
OutlinedTextField(
|
||||
value = opusPacketLoss,
|
||||
placeholder = { Text(stringResource(R.string.opus_packet_loss)) },
|
||||
onValueChange = {
|
||||
opusPacketLoss = it
|
||||
newOpusPacketLoss = opusPacketLoss
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.opus_packet_loss)
|
||||
alertMessage.value = getString(R.string.opus_packet_loss_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
|
||||
label = { LabelText(stringResource(R.string.opus_packet_loss)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AudioDelay() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
var audioDelay by remember { mutableStateOf(BaresipService.audioDelay.toString()) }
|
||||
newAudioDelay = audioDelay
|
||||
OutlinedTextField(
|
||||
value = audioDelay,
|
||||
placeholder = { Text(getString(R.string.audio_delay)) },
|
||||
onValueChange = {
|
||||
audioDelay = it
|
||||
newAudioDelay = audioDelay
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.audio_delay)
|
||||
alertMessage.value = getString(R.string.audio_delay_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
|
||||
label = { LabelText(stringResource(R.string.audio_delay)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToneCountry() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end=10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(text = stringResource(R.string.tone_country),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.tone_country)
|
||||
alertMessage.value = getString(R.string.tone_country_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
val isDropDownExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val countryNames = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US")
|
||||
val countryValues = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us")
|
||||
val itemPosition = remember {
|
||||
mutableIntStateOf(countryValues.indexOf(BaresipService.toneCountry))
|
||||
}
|
||||
Box {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable {
|
||||
isDropDownExpanded.value = true
|
||||
}
|
||||
) {
|
||||
Text(text = countryNames[itemPosition.intValue],
|
||||
color = LocalCustomColors.current.itemText)
|
||||
CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
|
||||
tint = LocalCustomColors.current.itemText)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = isDropDownExpanded.value,
|
||||
onDismissRequest = {
|
||||
isDropDownExpanded.value = false
|
||||
}) {
|
||||
countryNames.forEachIndexed { index, name ->
|
||||
DropdownMenuItem(text = {
|
||||
Text(text = name)
|
||||
},
|
||||
onClick = {
|
||||
isDropDownExpanded.value = false
|
||||
itemPosition.intValue = index
|
||||
newToneCountry = countryValues[index]
|
||||
})
|
||||
if (index < 10)
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOnClick() {
|
||||
|
||||
if (BaresipService.activities.indexOf("audio") == -1)
|
||||
return
|
||||
|
||||
if (BaresipService.callVolume != newCallVolume) {
|
||||
BaresipService.callVolume = newCallVolume
|
||||
Config.replaceVariable("call_volume", newCallVolume.toString())
|
||||
save = true
|
||||
}
|
||||
|
||||
if (!BaresipService.agcAvailable) {
|
||||
var gain = newMicGain.trim()
|
||||
if (!gain.contains("."))
|
||||
gain = "$gain.0"
|
||||
if (gain != oldMicGain) {
|
||||
if (!checkMicGain(gain)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = "${getString(R.string.invalid_microphone_gain)}: $gain."
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
if (gain == "1.0") {
|
||||
Api.module_unload("augain")
|
||||
Config.removeVariableValue("module", "augain.so")
|
||||
Config.replaceVariable("augain", "1.0")
|
||||
} else {
|
||||
if (oldMicGain == "1.0") {
|
||||
if (Api.module_load("augain") != 0) {
|
||||
alertTitle.value = getString(R.string.error)
|
||||
alertMessage.value = getString(R.string.failed_to_load_module) + ": augain.so"
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
Config.addVariable("module", "augain.so")
|
||||
}
|
||||
Config.replaceVariable("augain", gain)
|
||||
Api.cmd_exec("augain $gain")
|
||||
}
|
||||
save = true
|
||||
}
|
||||
}
|
||||
|
||||
if (newSpeakerPhone != BaresipService.speakerPhone) {
|
||||
BaresipService.speakerPhone = newSpeakerPhone
|
||||
Config.replaceVariable("speaker_phone",
|
||||
if (BaresipService.speakerPhone) "yes" else "no")
|
||||
save = true
|
||||
}
|
||||
|
||||
for (module in audioModules) {
|
||||
if (newAudioModules[module] != null) {
|
||||
if (newAudioModules[module]!!) {
|
||||
if (!modules.contains("${module}.so")) {
|
||||
if (Api.module_load("${module}.so") != 0) {
|
||||
alertTitle.value = getString(R.string.error)
|
||||
alertMessage.value = "${getString(R.string.failed_to_load_module)}: ${module}.so"
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
Config.addVariable("module", "${module}.so")
|
||||
save = true
|
||||
}
|
||||
} else if (modules.contains("${module}.so")) {
|
||||
Api.module_unload("${module}.so")
|
||||
Config.removeVariableValue("module", "${module}.so")
|
||||
for (ua in BaresipService.uas.value)
|
||||
ua.account.removeAudioCodecs(module)
|
||||
AccountsActivity.saveAccounts()
|
||||
save = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newOpusBitrate != oldOpusBitrate) {
|
||||
if (!checkOpusBitRate(newOpusBitrate)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = "${getString(R.string.invalid_opus_bitrate)}: $newOpusBitrate."
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
Config.replaceVariable("opus_bitrate", newOpusBitrate)
|
||||
restart = true
|
||||
save = true
|
||||
}
|
||||
|
||||
if (newOpusPacketLoss != oldOpusPacketLoss) {
|
||||
if (!checkOpusPacketLoss(newOpusPacketLoss)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = "${getString(R.string.invalid_opus_packet_loss)}: $newOpusPacketLoss"
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
Config.replaceVariable("opus_packet_loss", newOpusPacketLoss)
|
||||
restart = true
|
||||
save = true
|
||||
}
|
||||
|
||||
val audioDelay = newAudioDelay.trim()
|
||||
if (audioDelay != BaresipService.audioDelay.toString()) {
|
||||
if (!checkAudioDelay(audioDelay)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = String.format(getString(R.string.invalid_audio_delay), audioDelay)
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
Config.replaceVariable("audio_delay", audioDelay)
|
||||
BaresipService.audioDelay = audioDelay.toLong()
|
||||
save = true
|
||||
}
|
||||
|
||||
if (BaresipService.toneCountry != newToneCountry) {
|
||||
BaresipService.toneCountry = newToneCountry
|
||||
Config.replaceVariable("tone_country", newToneCountry)
|
||||
save = true
|
||||
}
|
||||
|
||||
if (save)
|
||||
Config.save()
|
||||
|
||||
setResult(if (restart) RESULT_OK else RESULT_CANCELED)
|
||||
|
||||
BaresipService.activities.remove("audio")
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("audio")
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun checkMicGain(micGain: String): Boolean {
|
||||
val number =
|
||||
try {
|
||||
micGain.toDouble()
|
||||
} catch (_: NumberFormatException) {
|
||||
return false
|
||||
}
|
||||
return number >= 1.0
|
||||
}
|
||||
|
||||
private fun checkOpusBitRate(opusBitRate: String): Boolean {
|
||||
val number = opusBitRate.toIntOrNull() ?: return false
|
||||
return (number >= 6000) && (number <= 510000)
|
||||
}
|
||||
|
||||
private fun checkOpusPacketLoss(opusPacketLoss: String): Boolean {
|
||||
val number = opusPacketLoss.toIntOrNull() ?: return false
|
||||
return (number >= 0) && (number <= 100)
|
||||
}
|
||||
|
||||
private fun checkAudioDelay(audioDelay: String): Boolean {
|
||||
val number = audioDelay.toIntOrNull() ?: return false
|
||||
return (number >= 100) && (number <= 3000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val audioModules = listOf("opus", "amr", "g722", "g7221", "g726", "g729", "codec2", "g711")
|
||||
}
|
||||
|
||||
}
|
||||
664
app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt
Normal file
664
app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt
Normal file
@ -0,0 +1,664 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.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.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
fun NavGraphBuilder.audioScreenRoute(
|
||||
navController: NavController,
|
||||
) {
|
||||
composable("audio") {
|
||||
val activity = LocalActivity.current
|
||||
val viewModel: ViewModel = viewModel(activity as ComponentActivity)
|
||||
val ctx = LocalContext.current
|
||||
AudioScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
checkOnClick = {
|
||||
viewModel.setAudioSettingsResult(checkOnClick(ctx))
|
||||
navController.popBackStack()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AudioScreen(
|
||||
onBack: () -> Unit,
|
||||
checkOnClick: () -> Unit,
|
||||
) {
|
||||
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.audio_settings),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
actions = {
|
||||
IconButton(onClick = checkOnClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { contentPadding ->
|
||||
AudioContent(contentPadding)
|
||||
}
|
||||
}
|
||||
|
||||
private var oldCallVolume = BaresipService.callVolume
|
||||
private var newCallVolume = oldCallVolume
|
||||
private var oldMicGain = ""
|
||||
private var newMicGain = ""
|
||||
private var oldSpeakerPhone = BaresipService.speakerPhone
|
||||
private var newSpeakerPhone = oldSpeakerPhone
|
||||
private var oldAudioModules = ArrayList<String>()
|
||||
private var newAudioModules = mutableMapOf<String, Boolean>()
|
||||
private var oldOpusBitrate = ""
|
||||
private var newOpusBitrate = oldOpusBitrate
|
||||
private var oldOpusPacketLoss = ""
|
||||
private var newOpusPacketLoss = oldOpusPacketLoss
|
||||
private var newAudioDelay = BaresipService.audioDelay.toString()
|
||||
private var newToneCountry = BaresipService.toneCountry
|
||||
|
||||
private var save = false
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
@Composable
|
||||
private fun AudioContent(contentPadding: PaddingValues) {
|
||||
|
||||
oldAudioModules = Config.variables("module")
|
||||
oldOpusBitrate = Config.variable("opus_bitrate")
|
||||
oldOpusPacketLoss = Config.variable("opus_packet_loss")
|
||||
if (!BaresipService.agcAvailable)
|
||||
oldMicGain = Config.variable("augain")
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 4.dp)
|
||||
.verticalScrollbar(scrollState)
|
||||
.verticalScroll(state = scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
CallVolume()
|
||||
MicGain()
|
||||
SpeakerPhone()
|
||||
AudioModules()
|
||||
OpusBitRate()
|
||||
OpusPacketLoss()
|
||||
AudioDelay()
|
||||
ToneCountry()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallVolume() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end=10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
Text(text = stringResource(R.string.default_call_volume),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.default_call_volume)
|
||||
alertMessage.value = ctx.getString(R.string.default_call_volume_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
val isDropDownExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val volNames = listOf("--", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
|
||||
val volValues = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
val itemPosition = remember {
|
||||
mutableIntStateOf(volValues.indexOf(oldCallVolume))
|
||||
}
|
||||
Box {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable {
|
||||
isDropDownExpanded.value = true
|
||||
}
|
||||
) {
|
||||
Text(text = volNames[itemPosition.intValue],
|
||||
color = LocalCustomColors.current.itemText)
|
||||
CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
|
||||
tint = LocalCustomColors.current.itemText)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = isDropDownExpanded.value,
|
||||
onDismissRequest = {
|
||||
isDropDownExpanded.value = false
|
||||
}) {
|
||||
volNames.forEachIndexed { index, vol ->
|
||||
DropdownMenuItem(text = {
|
||||
Text(text = vol)
|
||||
},
|
||||
onClick = {
|
||||
isDropDownExpanded.value = false
|
||||
itemPosition.intValue = index
|
||||
newCallVolume = volValues[index]
|
||||
})
|
||||
if (index < 10)
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MicGain() {
|
||||
if (!BaresipService.agcAvailable)
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
var micGain by remember { mutableStateOf(oldMicGain) }
|
||||
newMicGain = micGain
|
||||
OutlinedTextField(
|
||||
value = micGain,
|
||||
placeholder = { Text(stringResource(R.string.microphone_gain)) },
|
||||
onValueChange = {
|
||||
micGain = it
|
||||
newMicGain = micGain
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.microphone_gain)
|
||||
alertMessage.value = ctx.getString(R.string.microphone_gain_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { LabelText(stringResource(R.string.microphone_gain)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpeakerPhone() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end=10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
Text(text = stringResource(R.string.speaker_phone),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.speaker_phone)
|
||||
alertMessage.value = ctx.getString(R.string.speaker_phone_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) }
|
||||
Switch(
|
||||
checked = speakerPhone,
|
||||
onCheckedChange = {
|
||||
speakerPhone = it
|
||||
newSpeakerPhone = speakerPhone
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AudioModules() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
Text(text = stringResource(R.string.audio_modules_title),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp,
|
||||
modifier = Modifier.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.audio_modules_title)
|
||||
alertMessage.value = ctx.getString(R.string.audio_modules_help)
|
||||
showAlert.value = true
|
||||
})
|
||||
for (module in Config.audioModules) {
|
||||
Row(horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 18.dp, end = 10.dp)
|
||||
) {
|
||||
Text(text = String.format(ctx.getString(R.string.bullet_item), module),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var checked by remember { mutableStateOf(oldAudioModules.contains("${module}.so")) }
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
newAudioModules[module] = checked
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpusBitRate() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
var opusBitrate by remember { mutableStateOf(oldOpusBitrate) }
|
||||
newOpusBitrate = opusBitrate
|
||||
OutlinedTextField(
|
||||
value = opusBitrate,
|
||||
placeholder = { Text(stringResource(R.string.opus_bit_rate)) },
|
||||
onValueChange = {
|
||||
opusBitrate = it
|
||||
newOpusBitrate = opusBitrate
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.opus_bit_rate)
|
||||
alertMessage.value = ctx.getString(R.string.opus_bit_rate_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
|
||||
label = { LabelText(stringResource(R.string.opus_bit_rate)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpusPacketLoss() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
var opusPacketLoss by remember { mutableStateOf(oldOpusPacketLoss) }
|
||||
newOpusPacketLoss = opusPacketLoss
|
||||
OutlinedTextField(
|
||||
value = opusPacketLoss,
|
||||
placeholder = { Text(stringResource(R.string.opus_packet_loss)) },
|
||||
onValueChange = {
|
||||
opusPacketLoss = it
|
||||
newOpusPacketLoss = opusPacketLoss
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.opus_packet_loss)
|
||||
alertMessage.value = ctx.getString(R.string.opus_packet_loss_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
|
||||
label = { LabelText(stringResource(R.string.opus_packet_loss)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AudioDelay() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
var audioDelay by remember { mutableStateOf(BaresipService.audioDelay.toString()) }
|
||||
newAudioDelay = audioDelay
|
||||
OutlinedTextField(
|
||||
value = audioDelay,
|
||||
placeholder = { Text(stringResource(R.string.audio_delay)) },
|
||||
onValueChange = {
|
||||
audioDelay = it
|
||||
newAudioDelay = audioDelay
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.audio_delay)
|
||||
alertMessage.value = ctx.getString(R.string.audio_delay_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
|
||||
label = { LabelText(stringResource(R.string.audio_delay)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToneCountry() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(end=10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
Text(text = stringResource(R.string.tone_country),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.tone_country)
|
||||
alertMessage.value = ctx.getString(R.string.tone_country_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp)
|
||||
val isDropDownExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val countryNames = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US")
|
||||
val countryValues = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us")
|
||||
val itemPosition = remember {
|
||||
mutableIntStateOf(countryValues.indexOf(BaresipService.toneCountry))
|
||||
}
|
||||
Box {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable {
|
||||
isDropDownExpanded.value = true
|
||||
}
|
||||
) {
|
||||
Text(text = countryNames[itemPosition.intValue],
|
||||
color = LocalCustomColors.current.itemText)
|
||||
CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
|
||||
tint = LocalCustomColors.current.itemText)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = isDropDownExpanded.value,
|
||||
onDismissRequest = {
|
||||
isDropDownExpanded.value = false
|
||||
}) {
|
||||
countryNames.forEachIndexed { index, name ->
|
||||
DropdownMenuItem(text = {
|
||||
Text(text = name)
|
||||
},
|
||||
onClick = {
|
||||
isDropDownExpanded.value = false
|
||||
itemPosition.intValue = index
|
||||
newToneCountry = countryValues[index]
|
||||
})
|
||||
if (index < 10)
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOnClick(ctx: Context): Boolean {
|
||||
|
||||
var restart = false
|
||||
|
||||
if (BaresipService.callVolume != newCallVolume) {
|
||||
BaresipService.callVolume = newCallVolume
|
||||
Config.replaceVariable("call_volume", newCallVolume.toString())
|
||||
save = true
|
||||
}
|
||||
|
||||
if (!BaresipService.agcAvailable) {
|
||||
var gain = newMicGain.trim()
|
||||
if (!gain.contains("."))
|
||||
gain = "$gain.0"
|
||||
if (gain != oldMicGain) {
|
||||
if (!checkMicGain(gain)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = "${ctx.getString(R.string.invalid_microphone_gain)}: $gain."
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
if (gain == "1.0") {
|
||||
Api.module_unload("augain")
|
||||
Config.removeVariableValue("module", "augain.so")
|
||||
Config.replaceVariable("augain", "1.0")
|
||||
} else {
|
||||
if (oldMicGain == "1.0") {
|
||||
if (Api.module_load("augain") != 0) {
|
||||
alertTitle.value = ctx.getString(R.string.error)
|
||||
alertMessage.value = ctx.getString(R.string.failed_to_load_module) + ": augain.so"
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
Config.addVariable("module", "augain.so")
|
||||
}
|
||||
Config.replaceVariable("augain", gain)
|
||||
Api.cmd_exec("augain $gain")
|
||||
}
|
||||
save = true
|
||||
}
|
||||
}
|
||||
|
||||
if (newSpeakerPhone != BaresipService.speakerPhone) {
|
||||
BaresipService.speakerPhone = newSpeakerPhone
|
||||
Config.replaceVariable("speaker_phone",
|
||||
if (BaresipService.speakerPhone) "yes" else "no")
|
||||
save = true
|
||||
}
|
||||
|
||||
for (module in Config.audioModules) {
|
||||
if (newAudioModules[module] != null) {
|
||||
if (newAudioModules[module]!!) {
|
||||
if (!oldAudioModules.contains("${module}.so")) {
|
||||
if (Api.module_load("${module}.so") != 0) {
|
||||
alertTitle.value = ctx.getString(R.string.error)
|
||||
alertMessage.value = "${ctx.getString(R.string.failed_to_load_module)}: ${module}.so"
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
Config.addVariable("module", "${module}.so")
|
||||
save = true
|
||||
}
|
||||
} else if (oldAudioModules.contains("${module}.so")) {
|
||||
Api.module_unload("${module}.so")
|
||||
Config.removeVariableValue("module", "${module}.so")
|
||||
for (ua in BaresipService.uas.value)
|
||||
ua.account.removeAudioCodecs(module)
|
||||
Account.saveAccounts()
|
||||
save = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newOpusBitrate != oldOpusBitrate) {
|
||||
if (!checkOpusBitRate(newOpusBitrate)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = "${ctx.getString(R.string.invalid_opus_bitrate)}: $newOpusBitrate."
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
Config.replaceVariable("opus_bitrate", newOpusBitrate)
|
||||
restart = true
|
||||
save = true
|
||||
}
|
||||
|
||||
if (newOpusPacketLoss != oldOpusPacketLoss) {
|
||||
if (!checkOpusPacketLoss(newOpusPacketLoss)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = "${ctx.getString(R.string.invalid_opus_packet_loss)}: $newOpusPacketLoss"
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
Config.replaceVariable("opus_packet_loss", newOpusPacketLoss)
|
||||
restart = true
|
||||
save = true
|
||||
}
|
||||
|
||||
val audioDelay = newAudioDelay.trim()
|
||||
if (audioDelay != BaresipService.audioDelay.toString()) {
|
||||
if (!checkAudioDelay(audioDelay)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = String.format(ctx.getString(R.string.invalid_audio_delay), audioDelay)
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
Config.replaceVariable("audio_delay", audioDelay)
|
||||
BaresipService.audioDelay = audioDelay.toLong()
|
||||
save = true
|
||||
}
|
||||
|
||||
if (BaresipService.toneCountry != newToneCountry) {
|
||||
BaresipService.toneCountry = newToneCountry
|
||||
Config.replaceVariable("tone_country", newToneCountry)
|
||||
save = true
|
||||
}
|
||||
|
||||
if (save) Config.save()
|
||||
|
||||
return restart
|
||||
}
|
||||
|
||||
private fun checkMicGain(micGain: String): Boolean {
|
||||
val number =
|
||||
try {
|
||||
micGain.toDouble()
|
||||
} catch (_: NumberFormatException) {
|
||||
return false
|
||||
}
|
||||
return number >= 1.0
|
||||
}
|
||||
|
||||
private fun checkOpusBitRate(opusBitRate: String): Boolean {
|
||||
val number = opusBitRate.toIntOrNull() ?: return false
|
||||
return (number >= 6000) && (number <= 510000)
|
||||
}
|
||||
|
||||
private fun checkOpusPacketLoss(opusPacketLoss: String): Boolean {
|
||||
val number = opusPacketLoss.toIntOrNull() ?: return false
|
||||
return (number >= 0) && (number <= 100)
|
||||
}
|
||||
|
||||
private fun checkAudioDelay(audioDelay: String): Boolean {
|
||||
val number = audioDelay.toIntOrNull() ?: return false
|
||||
return (number >= 100) && (number <= 3000)
|
||||
}
|
||||
|
||||
@ -1,724 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds
|
||||
import android.provider.ContactsContract.Contacts.Data
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
class BaresipContactActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var name: String
|
||||
private lateinit var uri: String
|
||||
private var new = false
|
||||
private var favorite = false
|
||||
private var android = false
|
||||
private var newName = ""
|
||||
private var newUri = ""
|
||||
private var newFavorite = false
|
||||
private var newAndroid = false
|
||||
private var uriOrName = ""
|
||||
private var color = 0
|
||||
private var id: Long = 0
|
||||
private var newId: Long = 0
|
||||
private var tmpFile: File? = null
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
private val textAvatarText = mutableStateOf("")
|
||||
private val textAvatarColor = mutableIntStateOf(0)
|
||||
private val imageAvatarUri = mutableStateOf("")
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
val title: String
|
||||
|
||||
new = intent.getBooleanExtra("new", false)
|
||||
|
||||
if (new) {
|
||||
name = ""
|
||||
uri = intent.getStringExtra("uri")!!
|
||||
favorite = false
|
||||
android = BaresipService.contactsMode == "android"
|
||||
title = getString(R.string.new_contact)
|
||||
uriOrName = uri
|
||||
color = Utils.randomColor()
|
||||
id = System.currentTimeMillis()
|
||||
newId = id
|
||||
}
|
||||
else {
|
||||
name = intent.getStringExtra("name")!!
|
||||
val contact = Contact.baresipContact(name)!!
|
||||
uri = contact.uri
|
||||
favorite = contact.favorite
|
||||
android = false
|
||||
title = name
|
||||
uriOrName = name
|
||||
color = contact.color
|
||||
id = contact.id
|
||||
newId = id
|
||||
if (contact.avatarImage != null) {
|
||||
val avatarFile = File(BaresipService.filesPath, "${newId}.png")
|
||||
if (avatarFile.exists())
|
||||
imageAvatarUri.value = Uri.fromFile(avatarFile).toString()
|
||||
}
|
||||
}
|
||||
|
||||
Utils.addActivity("baresip contact,$new,$uriOrName")
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ContactScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxHeight().imePadding().safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(ctx, title, navigateBack) },
|
||||
content = { contentPadding ->
|
||||
ContactContent(contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
checkOnClick(ctx)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ContactContent(contentPadding: PaddingValues) {
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Avatar()
|
||||
ContactName()
|
||||
ContactUri()
|
||||
Favorite()
|
||||
Android()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun TextAvatar(size: Int) {
|
||||
Box(
|
||||
modifier = Modifier.size(size.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(SolidColor(Color(textAvatarColor.intValue)))
|
||||
}
|
||||
Text(textAvatarText.value, fontSize = 72.sp, color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageAvatar(size: Int) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(model = imageAvatarUri.value),
|
||||
contentDescription = stringResource(R.string.avatar_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(size.dp).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Avatar() {
|
||||
val avatarRequest =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
if (it != null)
|
||||
try {
|
||||
val inputStream = baseContext.contentResolver.openInputStream(it)
|
||||
val avatarBitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream?.close()
|
||||
val scaledBitmap = avatarBitmap.scale(192, 192)
|
||||
val exif = ExifInterface(baseContext.contentResolver.openInputStream(it)!!)
|
||||
val orientation = exif.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL
|
||||
)
|
||||
val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
|
||||
if (tmpFile != null && tmpFile!!.exists())
|
||||
Utils.deleteFile(tmpFile!!)
|
||||
newId = System.currentTimeMillis()
|
||||
tmpFile = File(BaresipService.filesPath, "${newId}.png")
|
||||
if (Utils.saveBitmap(rotatedBitmap, tmpFile!!)) {
|
||||
imageAvatarUri.value = Uri.fromFile(tmpFile).toString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not read avatar image: ${e.message}")
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(96.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
avatarRequest.launch("image/*")
|
||||
},
|
||||
onLongClick = {
|
||||
textAvatarColor.intValue = Utils.randomColor()
|
||||
color = textAvatarColor.intValue
|
||||
imageAvatarUri.value = ""
|
||||
}
|
||||
),
|
||||
) {
|
||||
if (imageAvatarUri.value == "") {
|
||||
textAvatarText.value = if (new)
|
||||
"?"
|
||||
else
|
||||
"${name[0]}"
|
||||
textAvatarColor.intValue = color
|
||||
TextAvatar(96)
|
||||
} else
|
||||
ImageAvatar(96)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactName() {
|
||||
val focusRequester = FocusRequester()
|
||||
var contactName by remember { mutableStateOf(name) }
|
||||
newName = contactName
|
||||
OutlinedTextField(
|
||||
value = contactName,
|
||||
placeholder = { Text(stringResource(R.string.contact_name)) },
|
||||
onValueChange = {
|
||||
contactName = it
|
||||
newName = contactName
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { Text(stringResource(R.string.contact_name)) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
capitalization = KeyboardCapitalization.Words
|
||||
)
|
||||
)
|
||||
LaunchedEffect(new) {
|
||||
if (new)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactUri() {
|
||||
var contactUri by remember { mutableStateOf(uri) }
|
||||
newUri = contactUri
|
||||
OutlinedTextField(
|
||||
value = contactUri,
|
||||
placeholder = { Text(stringResource(R.string.user_domain_or_number)) },
|
||||
onValueChange = {
|
||||
contactUri = it
|
||||
newUri = contactUri
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { Text(stringResource(R.string.sip_or_tel_uri)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Favorite() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.favorite),
|
||||
modifier = Modifier.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = getString(R.string.favorite)
|
||||
alertMessage.value = getString(R.string.favorite_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
)
|
||||
var favoriteContact by remember { mutableStateOf(favorite) }
|
||||
newFavorite = favoriteContact
|
||||
Switch(
|
||||
checked = favoriteContact,
|
||||
onCheckedChange = {
|
||||
favoriteContact = it
|
||||
newFavorite = favoriteContact
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Android() {
|
||||
if (new && BaresipService.contactsMode != "baresip")
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.android),
|
||||
modifier = Modifier.weight(1f),
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
var androidContact by remember { mutableStateOf(android) }
|
||||
newAndroid = androidContact
|
||||
Switch(
|
||||
checked = androidContact,
|
||||
onCheckedChange = {
|
||||
if (BaresipService.contactsMode != "android") {
|
||||
androidContact = it
|
||||
newAndroid = androidContact
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOnClick(ctx: Context) {
|
||||
if (BaresipService.activities.indexOf("baresip contact,$new,$uriOrName") == -1)
|
||||
return
|
||||
|
||||
newUri = newUri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
||||
if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
|
||||
newUri = if (Utils.isTelNumber(newUri))
|
||||
"tel:$newUri"
|
||||
else
|
||||
"sip:$newUri"
|
||||
if (!Utils.checkUri(newUri)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), newUri)
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
|
||||
newName = newName.trim()
|
||||
if (newName == "") newName = newUri.substringAfter(":")
|
||||
if (!Utils.checkName(newName)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = String.format(getString(R.string.invalid_contact), newName)
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
|
||||
val alert: Boolean = if (new)
|
||||
Contact.nameExists(newName, BaresipService.contacts,true)
|
||||
else {
|
||||
(uriOrName != newName) && Contact.nameExists(newName, BaresipService.contacts, false)
|
||||
}
|
||||
if (alert) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = String.format(getString(R.string.contact_already_exists), newName)
|
||||
showAlert.value = true
|
||||
return
|
||||
}
|
||||
|
||||
val contact: Contact.BaresipContact =
|
||||
Contact.BaresipContact(newName, newUri, color, newId, newFavorite)
|
||||
|
||||
if (imageAvatarUri.value == "") {
|
||||
if (contact.avatarImage != null) {
|
||||
contact.avatarImage = null
|
||||
Utils.deleteFile(File(BaresipService.filesPath, "${newId}.png"))
|
||||
}
|
||||
}
|
||||
else {
|
||||
val imageFilePath = BaresipService.filesPath + "/${newId}.png"
|
||||
contact.avatarImage = BitmapFactory.decodeFile(imageFilePath)
|
||||
}
|
||||
|
||||
if (newAndroid)
|
||||
addOrUpdateAndroidContact(ctx, contact)
|
||||
else {
|
||||
if (new)
|
||||
Contact.addBaresipContact(contact)
|
||||
else
|
||||
Contact.updateBaresipContact(id, contact)
|
||||
}
|
||||
|
||||
BaresipService.activities.remove("baresip contact,$new,$uriOrName")
|
||||
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
if (newAndroid && contact.favorite)
|
||||
i.putExtra("name", newName)
|
||||
setResult(RESULT_OK, i)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
|
||||
val matrix = Matrix()
|
||||
when (orientation) {
|
||||
ExifInterface.ORIENTATION_NORMAL -> return bitmap
|
||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
|
||||
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
||||
matrix.setRotate(180f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||
matrix.setRotate(90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
|
||||
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||
matrix.setRotate(-90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
|
||||
else -> return bitmap
|
||||
}
|
||||
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
|
||||
bitmap.width, bitmap.height, matrix, true)
|
||||
bitmap.recycle()
|
||||
return rotatedBitmap
|
||||
}
|
||||
|
||||
private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) {
|
||||
val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID)
|
||||
val selection = ContactsContract.Data.MIMETYPE + "='" +
|
||||
CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
|
||||
CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
|
||||
val c: Cursor? = ctx.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI, projection,
|
||||
selection, null, null)
|
||||
if (c != null && c.moveToFirst()) {
|
||||
updateAndroidContact(c.getLong(0), contact)
|
||||
} else {
|
||||
addAndroidContact(ctx, contact)
|
||||
}
|
||||
c?.close()
|
||||
}
|
||||
|
||||
private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build())
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
|
||||
.build())
|
||||
val mimeType = if (contact.uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, contact.uri.substringAfter(":"))
|
||||
.build())
|
||||
|
||||
if (contact.avatarImage != null) {
|
||||
val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
|
||||
if (photoData != null) {
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoData)
|
||||
.build())
|
||||
}
|
||||
}
|
||||
try {
|
||||
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of contact ${contact.name} failed: ${e.message}")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateAndroidContact(rawContactId: Long, contact: Contact.BaresipContact) {
|
||||
if (updateAndroidUri(rawContactId, contact.uri) == 0)
|
||||
addAndroidUri(rawContactId, contact.uri)
|
||||
if (updateAndroidPhoto(rawContactId, contact.avatarImage) == 0)
|
||||
if (contact.avatarImage != null)
|
||||
addAndroidPhoto(rawContactId, contact.avatarImage!!)
|
||||
}
|
||||
|
||||
private fun addAndroidUri(rawContactId: Long, uri: String) {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, uri.substringAfter(":"))
|
||||
.build())
|
||||
try {
|
||||
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of SIP URI $uri failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidUri(rawContactId: Long, uri: String): Int {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(ContactsContract.Data.DATA1, uri)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='$mimeType'"
|
||||
return try {
|
||||
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Update of Android URI $uri failed: ${e.message}")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAndroidPhoto(rawContactId: Long, photoBits: Bitmap) {
|
||||
val photoBytes = bitmapToPNGByteArray(photoBits)
|
||||
if (photoBytes != null) {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
.build())
|
||||
try {
|
||||
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of Android photo failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidPhoto(rawContactId: Long, photoBits: Bitmap?): Int {
|
||||
val photoBytes = if (photoBits == null)
|
||||
null
|
||||
else
|
||||
bitmapToPNGByteArray(photoBits)
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
|
||||
return try {
|
||||
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "updateAndroidPhoto failed: ${e.message}")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? {
|
||||
val size = bitmap.width * bitmap.height * 4
|
||||
val out = ByteArrayOutputStream(size)
|
||||
return try {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
out.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to serialize photo: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("baresip contact,$new,$uriOrName")
|
||||
setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
819
app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt
Normal file
819
app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt
Normal file
@ -0,0 +1,819 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds
|
||||
import android.provider.ContactsContract.Contacts.Data
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
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.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
fun NavGraphBuilder.baresipContactScreenRoute(navController: NavController) {
|
||||
composable(
|
||||
route = "baresip_contact/{uri_or_name}/{kind}",
|
||||
arguments = listOf(
|
||||
navArgument("uri_or_name") { type = NavType.StringType },
|
||||
navArgument("kind") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val uriOrNameArg = backStackEntry.arguments?.getString("uri_or_name")!!
|
||||
val kindArg = backStackEntry.arguments?.getString("kind")!!
|
||||
ContactScreen(
|
||||
navController = navController,
|
||||
uriOrNameArg = uriOrNameArg,
|
||||
kindArg = kindArg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ScreenState(
|
||||
val new: Boolean = false,
|
||||
val favorite: Boolean = false,
|
||||
val android: Boolean = false,
|
||||
val id: Long = 0,
|
||||
val newId: Long = 0,
|
||||
val name: String = "",
|
||||
val uri: String = "",
|
||||
val color: Int = 0,
|
||||
val avatarImageUri: String? = null,
|
||||
val tmpAvatarFile: File? = null,
|
||||
val isLoading: Boolean = true
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContactScreen(
|
||||
navController: NavController,
|
||||
uriOrNameArg: String,
|
||||
kindArg: String
|
||||
) {
|
||||
|
||||
val ctx = LocalContext.current
|
||||
|
||||
var screenState by remember { mutableStateOf(ScreenState()) }
|
||||
|
||||
val title = if (screenState.new)
|
||||
stringResource(R.string.new_contact)
|
||||
else
|
||||
uriOrNameArg
|
||||
|
||||
LaunchedEffect(key1 = uriOrNameArg, key2 = kindArg) {
|
||||
val isNew = kindArg == "new"
|
||||
if (isNew) {
|
||||
val time = System.currentTimeMillis()
|
||||
screenState = ScreenState(
|
||||
new = true,
|
||||
name = "",
|
||||
uri = uriOrNameArg,
|
||||
favorite = false,
|
||||
android = BaresipService.contactsMode == "android",
|
||||
color = Utils.randomColor(),
|
||||
id = time,
|
||||
newId = time,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
else {
|
||||
val contact = Contact.baresipContact(uriOrNameArg)!!
|
||||
val avatarFile = File(BaresipService.filesPath, "${contact.id}.png")
|
||||
screenState = ScreenState(
|
||||
new = false,
|
||||
name = uriOrNameArg,
|
||||
uri = contact.uri,
|
||||
favorite = contact.favorite,
|
||||
android = false,
|
||||
color = contact.color,
|
||||
id = contact.id,
|
||||
newId = contact.id,
|
||||
avatarImageUri = if (contact.avatarImage != null && avatarFile.exists())
|
||||
Uri.fromFile(avatarFile).toString()
|
||||
else
|
||||
null,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val onBack: () -> Unit = {
|
||||
screenState.tmpAvatarFile?.let { tempFile ->
|
||||
if (tempFile.exists()) {
|
||||
Log.d(TAG, "Back pressed, deleting temp avatar: ${tempFile.name}")
|
||||
Utils.deleteFile(tempFile)
|
||||
}
|
||||
}
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
val onCheck: () -> Unit = {
|
||||
val result = checkOnClick(
|
||||
ctx = ctx,
|
||||
currentState = screenState,
|
||||
uriOrNameArg = uriOrNameArg,
|
||||
)
|
||||
if (result)
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
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, onBack = onBack, onCheck = onCheck)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
if (!screenState.isLoading) {
|
||||
ContactContent(
|
||||
contentPadding = contentPadding,
|
||||
screenState = screenState,
|
||||
onStateChange = { newState -> screenState = newState }
|
||||
)
|
||||
} else {
|
||||
// Optional: Show a loading indicator
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopAppBar(title: String, onBack: () -> Unit, onCheck: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onCheck) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ContactContent(
|
||||
contentPadding: PaddingValues,
|
||||
screenState: ScreenState,
|
||||
onStateChange: (ScreenState) -> Unit
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Avatar(
|
||||
ctx = ctx,
|
||||
name = screenState.name,
|
||||
color = screenState.color,
|
||||
currentAvatarUri = screenState.avatarImageUri,
|
||||
onNewAvatarChosen = { processedTmpFile, generatedNewId ->
|
||||
onStateChange(
|
||||
screenState.copy(
|
||||
avatarImageUri = Uri.fromFile(processedTmpFile).toString(),
|
||||
tmpAvatarFile = processedTmpFile,
|
||||
newId = generatedNewId
|
||||
)
|
||||
)
|
||||
},
|
||||
onAvatarColorChange = { newRandomColor ->
|
||||
// User long-clicked to change color, this means discarding any image.
|
||||
// The old tempAvatarFile (if any) should be deleted.
|
||||
screenState.tmpAvatarFile?.let {
|
||||
if (it.exists())
|
||||
Utils.deleteFile(it)
|
||||
}
|
||||
onStateChange(
|
||||
screenState.copy(
|
||||
color = newRandomColor,
|
||||
avatarImageUri = null,
|
||||
tmpAvatarFile = null
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
ContactName(
|
||||
name = screenState.name,
|
||||
new = screenState.new,
|
||||
onNameChange = { newName -> onStateChange(screenState.copy(name = newName)) }
|
||||
)
|
||||
ContactUri(
|
||||
uri = screenState.uri,
|
||||
onUriChange = { newUri -> onStateChange(screenState.copy(uri = newUri)) }
|
||||
)
|
||||
Favorite(
|
||||
ctx = ctx,
|
||||
favorite = screenState.favorite,
|
||||
onFavoriteChange = {
|
||||
newFavorite -> onStateChange(screenState.copy(favorite = newFavorite))
|
||||
}
|
||||
)
|
||||
if (screenState.new && BaresipService.contactsMode == "both")
|
||||
Android(
|
||||
android = screenState.android,
|
||||
onAndroidChange = { newAndroid -> onStateChange(screenState.copy(android = newAndroid)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun Avatar(
|
||||
ctx: Context,
|
||||
name: String,
|
||||
color: Int,
|
||||
currentAvatarUri: String?,
|
||||
onNewAvatarChosen: (newImageFile: File, newImageId: Long) -> Unit,
|
||||
onAvatarColorChange: (newColor: Int) -> Unit
|
||||
) {
|
||||
val avatarImagePicker =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val inputStream = ctx.contentResolver.openInputStream(uri)
|
||||
val avatarBitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream?.close()
|
||||
|
||||
if (avatarBitmap == null) {
|
||||
Log.e(TAG, "Failed to decode bitmap from URI: $uri")
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
val scaledBitmap = avatarBitmap.scale(192, 192) // Define desired scale
|
||||
|
||||
val orientationInputStream = ctx.contentResolver.openInputStream(uri)
|
||||
val exif = if (orientationInputStream != null) ExifInterface(orientationInputStream) else null
|
||||
orientationInputStream?.close()
|
||||
val orientation = exif?.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL
|
||||
) ?: ExifInterface.ORIENTATION_NORMAL
|
||||
val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
|
||||
|
||||
val newImageId = System.currentTimeMillis()
|
||||
val tempNewImageFile = File(BaresipService.filesPath, "${newImageId}.png")
|
||||
|
||||
if (saveBitmap(rotatedBitmap, tempNewImageFile)) {
|
||||
onNewAvatarChosen(tempNewImageFile, newImageId)
|
||||
} else {
|
||||
Log.e(TAG, "Failed to save processed avatar image to ${tempNewImageFile.absolutePath}")
|
||||
if (tempNewImageFile.exists()) Utils.deleteFile(tempNewImageFile)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not process avatar image: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(avatarSize.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (currentAvatarUri == null) Color(color) else Color.Transparent)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
avatarImagePicker.launch("image/*")
|
||||
},
|
||||
onLongClick = {
|
||||
onAvatarColorChange(Utils.randomColor())
|
||||
}
|
||||
)
|
||||
) {
|
||||
if (currentAvatarUri == null) {
|
||||
Box(
|
||||
modifier = Modifier.size(avatarSize.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(SolidColor(Color(color)))
|
||||
}
|
||||
val text = if (name.isNotBlank()) name.substring(0, 1).uppercase() else "?"
|
||||
Text(text, fontSize = 72.sp, color = Color.White)
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(model = currentAvatarUri),
|
||||
contentDescription = stringResource(R.string.avatar_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(avatarSize.dp).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactName(name: String, new: Boolean, onNameChange: (String) -> Unit) {
|
||||
val focusRequester = FocusRequester()
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
placeholder = { Text(stringResource(R.string.contact_name)) },
|
||||
onValueChange = onNameChange,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { Text(stringResource(R.string.contact_name)) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
capitalization = KeyboardCapitalization.Words
|
||||
)
|
||||
)
|
||||
LaunchedEffect(new) {
|
||||
if (new)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactUri(uri: String, onUriChange: (String) -> Unit) {
|
||||
OutlinedTextField(
|
||||
value = uri,
|
||||
placeholder = { Text(stringResource(R.string.user_domain_or_number)) },
|
||||
onValueChange = onUriChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { Text(stringResource(R.string.sip_or_tel_uri)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Favorite(ctx: Context, favorite: Boolean, onFavoriteChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.favorite),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
alertTitle.value = ctx.getString(R.string.favorite)
|
||||
alertMessage.value = ctx.getString(R.string.favorite_help)
|
||||
showAlert.value = true
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
)
|
||||
Switch(
|
||||
checked = favorite,
|
||||
onCheckedChange = onFavoriteChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Android(android: Boolean, onAndroidChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.android),
|
||||
modifier = Modifier.weight(1f),
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
Switch(
|
||||
checked = android,
|
||||
onCheckedChange = onAndroidChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOnClick(
|
||||
ctx: Context,
|
||||
currentState: ScreenState,
|
||||
uriOrNameArg: String
|
||||
): Boolean {
|
||||
|
||||
var newUri = currentState.uri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
||||
if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
|
||||
newUri = if (Utils.isTelNumber(newUri))
|
||||
"tel:$newUri"
|
||||
else
|
||||
"sip:$newUri"
|
||||
|
||||
if (!Utils.checkUri(newUri)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), newUri)
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
|
||||
var newName = currentState.name.trim()
|
||||
if (newName == "") newName = newUri.substringAfter(":")
|
||||
if (!Utils.checkName(newName)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = String.format(ctx.getString(R.string.invalid_contact), newName)
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
|
||||
val alert: Boolean = if (currentState.new)
|
||||
Contact.nameExists(newName, BaresipService.contacts, true)
|
||||
else {
|
||||
(uriOrNameArg != newName) && Contact.nameExists(newName, BaresipService.contacts, false)
|
||||
}
|
||||
if (alert) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = String.format(ctx.getString(R.string.contact_already_exists), newName)
|
||||
showAlert.value = true
|
||||
return false
|
||||
}
|
||||
|
||||
var idToUse = currentState.id
|
||||
|
||||
if (currentState.tmpAvatarFile != null && currentState.tmpAvatarFile.exists()) {
|
||||
if (currentState.id != currentState.newId) { // Avatar changed, implying new ID
|
||||
// Delete old avatar if it existed for currentState.contactId
|
||||
val oldAvatar = File(BaresipService.filesPath, "${currentState.id}.png")
|
||||
if (oldAvatar.exists()) Utils.deleteFile(oldAvatar)
|
||||
idToUse = currentState.newId // Use the new ID for the contact
|
||||
}
|
||||
/*val avatarFile = File(BaresipService.filesPath, "$idToUse.png")
|
||||
if (!Utils.moveFile(currentState.tempAvatarFile, avatarFile)) {
|
||||
Log.e(TAG, "Failed to move tmp avatar file $idToUse.png")
|
||||
return "Failed to save avatar"
|
||||
}*/
|
||||
} else if (currentState.avatarImageUri == null) { // Avatar was explicitly cleared
|
||||
val avatarFile = File(BaresipService.filesPath, "$idToUse.png")
|
||||
if (avatarFile.exists())
|
||||
Utils.deleteFile(avatarFile)
|
||||
}
|
||||
|
||||
val contact: Contact.BaresipContact =
|
||||
Contact.BaresipContact(
|
||||
newName,
|
||||
newUri,
|
||||
currentState.color,
|
||||
idToUse,
|
||||
currentState.favorite
|
||||
)
|
||||
|
||||
if (currentState.avatarImageUri == null)
|
||||
contact.avatarImage = null
|
||||
else {
|
||||
val imageFilePath = BaresipService.filesPath + "/${idToUse}.png"
|
||||
contact.avatarImage = BitmapFactory.decodeFile(imageFilePath)
|
||||
}
|
||||
|
||||
if (currentState.android) {
|
||||
addOrUpdateAndroidContact(ctx, contact)
|
||||
if (contact.favorite) {
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(ContactsContract.Contacts.STARRED, 1)
|
||||
try {
|
||||
ctx.contentResolver.update(
|
||||
ContactsContract.RawContacts.CONTENT_URI, contentValues,
|
||||
ContactsContract.Contacts.DISPLAY_NAME + "='" + newName + "'", null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Update of Android favorite failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (currentState.new)
|
||||
Contact.addBaresipContact(contact)
|
||||
else
|
||||
Contact.updateBaresipContact(currentState.id, contact)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
|
||||
val matrix = Matrix()
|
||||
when (orientation) {
|
||||
ExifInterface.ORIENTATION_NORMAL -> return bitmap
|
||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
|
||||
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
||||
matrix.setRotate(180f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||
matrix.setRotate(90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
|
||||
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||
matrix.setRotate(-90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
|
||||
else -> return bitmap
|
||||
}
|
||||
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
|
||||
bitmap.width, bitmap.height, matrix, true)
|
||||
bitmap.recycle()
|
||||
return rotatedBitmap
|
||||
}
|
||||
|
||||
private fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
|
||||
if (file.exists()) file.delete()
|
||||
try {
|
||||
val out = FileOutputStream(file)
|
||||
val scaledBitmap = bitmap.scale(avatarSize, avatarSize)
|
||||
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
Log.d(TAG, "Saved bitmap to ${file.absolutePath} of length ${file.length()}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save bitmap to ${file.absolutePath}: $e")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) {
|
||||
val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID)
|
||||
val selection = ContactsContract.Data.MIMETYPE + "='" +
|
||||
CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
|
||||
CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
|
||||
val c: Cursor? = ctx.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI, projection,
|
||||
selection, null, null)
|
||||
if (c != null && c.moveToFirst()) {
|
||||
updateAndroidContact(ctx, c.getLong(0), contact)
|
||||
} else {
|
||||
addAndroidContact(ctx, contact)
|
||||
}
|
||||
c?.close()
|
||||
}
|
||||
|
||||
private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build())
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
|
||||
.build())
|
||||
val mimeType = if (contact.uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, contact.uri.substringAfter(":"))
|
||||
.build())
|
||||
|
||||
if (contact.avatarImage != null) {
|
||||
val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
|
||||
if (photoData != null) {
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoData)
|
||||
.build())
|
||||
}
|
||||
}
|
||||
try {
|
||||
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of contact ${contact.name} failed: ${e.message}")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) {
|
||||
if (updateAndroidUri(ctx, rawContactId, contact.uri) == 0)
|
||||
addAndroidUri(ctx, rawContactId, contact.uri)
|
||||
if (updateAndroidPhoto(ctx, rawContactId, contact.avatarImage) == 0)
|
||||
if (contact.avatarImage != null)
|
||||
addAndroidPhoto(ctx, rawContactId, contact.avatarImage!!)
|
||||
}
|
||||
|
||||
private fun addAndroidUri(ctx: Context, rawContactId: Long, uri: String) {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, uri.substringAfter(":"))
|
||||
.build())
|
||||
try {
|
||||
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of SIP URI $uri failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidUri(ctx: Context, rawContactId: Long, uri: String): Int {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(ContactsContract.Data.DATA1, uri)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='$mimeType'"
|
||||
return try {
|
||||
ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Update of Android URI $uri failed: ${e.message}")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap) {
|
||||
val photoBytes = bitmapToPNGByteArray(photoBits)
|
||||
if (photoBytes != null) {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
.build())
|
||||
try {
|
||||
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of Android photo failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap?): Int {
|
||||
val photoBytes = if (photoBits == null)
|
||||
null
|
||||
else
|
||||
bitmapToPNGByteArray(photoBits)
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
|
||||
return try {
|
||||
ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "updateAndroidPhoto failed: ${e.message}")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? {
|
||||
val size = bitmap.width * bitmap.height * 4
|
||||
val out = ByteArrayOutputStream(size)
|
||||
return try {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
out.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to serialize photo: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
@ -49,21 +49,19 @@ import android.provider.ContactsContract
|
||||
import android.provider.Settings
|
||||
import android.system.OsConstants
|
||||
import android.telecom.TelecomManager
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.MessagingStyle
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@ -553,8 +551,13 @@ class BaresipService: Service() {
|
||||
val ua = UserAgent.ofUap(uap)
|
||||
if (ua == null)
|
||||
Log.w(TAG, "onStartCommand did not find UA $uap")
|
||||
else
|
||||
Message.updateAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
|
||||
else {
|
||||
Message.updateAorMessage(
|
||||
ua.account.aor,
|
||||
intent.getStringExtra("time")!!.toLong()
|
||||
)
|
||||
ua.account.unreadMessages = Message.unreadMessages(ua.account.aor)
|
||||
}
|
||||
nm.cancel(MESSAGE_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
@ -563,9 +566,50 @@ class BaresipService: Service() {
|
||||
val ua = UserAgent.ofUap(uap)
|
||||
if (ua == null)
|
||||
Log.w(TAG, "onStartCommand did not find UA $uap")
|
||||
else
|
||||
Message.deleteAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
|
||||
else {
|
||||
Message.deleteAorMessage(
|
||||
ua.account.aor,
|
||||
intent.getStringExtra("time")!!.toLong()
|
||||
)
|
||||
ua.account.unreadMessages = Message.unreadMessages(ua.account.aor)
|
||||
}
|
||||
nm.cancel(MESSAGE_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
"Message Inline Reply" -> {
|
||||
val remoteInputResults = RemoteInput.getResultsFromIntent(intent!!)
|
||||
if (remoteInputResults != null) {
|
||||
val replyText = remoteInputResults.getCharSequence(KEY_TEXT_REPLY)?.toString()
|
||||
if (!replyText.isNullOrEmpty()) {
|
||||
val uap = intent.getLongExtra("uap", -1L)
|
||||
val ua = UserAgent.ofUap(uap)!!
|
||||
val aor = ua.account.aor
|
||||
var peerUri = intent.getStringExtra("peer")!!
|
||||
val timeStamp = intent.getLongExtra("time", 0L)
|
||||
if (Utils.isTelUri(peerUri)) {
|
||||
if (ua.account.telProvider == "") {
|
||||
Log.w(TAG, "No telephony provider for $aor")
|
||||
peerUri = ""
|
||||
} else
|
||||
peerUri = Utils.telToSip(peerUri, ua.account)
|
||||
}
|
||||
if (peerUri != "") {
|
||||
Log.d(TAG, "Direct Reply from $aor to $peerUri: $replyText")
|
||||
Message.updateAorMessage(aor, timeStamp)
|
||||
val time = System.currentTimeMillis()
|
||||
val msg = Message(aor, peerUri, replyText, time, MESSAGE_UP_WAIT, 0, "", false)
|
||||
msg.add()
|
||||
if (Api.message_send(uap, peerUri, replyText, time.toString()) != 0) {
|
||||
Log.w(TAG, "message_send failed")
|
||||
msg.direction = MESSAGE_UP_FAIL
|
||||
msg.responseReason = getString(R.string.message_failed)
|
||||
}
|
||||
else {
|
||||
ua.account.unreadMessages = Message.unreadMessages(aor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nm.cancel(MESSAGE_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
@ -844,15 +888,15 @@ class BaresipService: Service() {
|
||||
if (!Utils.isVisible()) {
|
||||
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
.putExtra("action", "call show")
|
||||
.putExtra("callp", callp)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.putExtra("action", "call show")
|
||||
.putExtra("callp", callp)
|
||||
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent,
|
||||
piFlags)
|
||||
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
|
||||
val nb = NotificationCompat.Builder(this,
|
||||
if (shouldVibrate()) MEDIUM_CHANNEL_ID else HIGH_CHANNEL_ID)
|
||||
val caller = Utils.friendlyUri(this, peerUri, ua.account)
|
||||
val person = Person.Builder().setName(caller).build()
|
||||
nb.setSmallIcon(R.drawable.ic_stat_call)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorBaresip))
|
||||
.setContentIntent(pi)
|
||||
@ -867,19 +911,15 @@ class BaresipService: Service() {
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setFullScreenIntent(pi, true)
|
||||
val answerIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
answerIntent.putExtra("action", "call answer")
|
||||
.putExtra("action", "call answer")
|
||||
.putExtra("callp", callp)
|
||||
val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE,
|
||||
answerIntent, piFlags)
|
||||
val rejectIntent = Intent(this, BaresipService::class.java)
|
||||
rejectIntent.action = "Call Reject"
|
||||
rejectIntent.putExtra("callp", callp)
|
||||
val rpi = PendingIntent.getService(this, REJECT_REQ_CODE,
|
||||
rejectIntent, piFlags)
|
||||
nb.addAction(R.drawable.ic_stat_call,
|
||||
getActionText(R.string.answer, R.color.colorGreen), api)
|
||||
nb.addAction(R.drawable.ic_stat_call_end,
|
||||
getActionText(R.string.reject, R.color.colorRed), rpi)
|
||||
val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags)
|
||||
nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api))
|
||||
nm.notify(CALL_NOTIFICATION_ID, nb.build())
|
||||
return
|
||||
}
|
||||
@ -938,8 +978,7 @@ class BaresipService: Service() {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.putExtra("action", "transfer show")
|
||||
.putExtra("callp", callp).putExtra("uri", ev[1])
|
||||
val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE,
|
||||
intent, piFlags)
|
||||
val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE, intent, piFlags)
|
||||
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
||||
val target = Utils.friendlyUri(this, ev[1], ua.account)
|
||||
nb.setSmallIcon(R.drawable.ic_stat_call)
|
||||
@ -952,7 +991,7 @@ class BaresipService: Service() {
|
||||
val acceptIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
acceptIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
acceptIntent.putExtra("action","transfer accept")
|
||||
acceptIntent.putExtra("action", "transfer accept")
|
||||
.putExtra("callp", callp).putExtra("uri", ev[1])
|
||||
val acceptPendingIntent = PendingIntent.getActivity(applicationContext,
|
||||
ACCEPT_REQ_CODE, acceptIntent, piFlags)
|
||||
@ -1078,16 +1117,6 @@ class BaresipService: Service() {
|
||||
|
||||
}
|
||||
|
||||
private fun postServiceEvent(event: ServiceEvent) {
|
||||
serviceEvents.add(event)
|
||||
if (serviceEvents.size == 1) {
|
||||
Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}")
|
||||
serviceEvent.postValue(Event(event.timeStamp))
|
||||
} else {
|
||||
Log.d(TAG, "Added service event ${event.event}")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
@Keep
|
||||
@ -1120,45 +1149,88 @@ class BaresipService: Service() {
|
||||
ua.account.unreadMessages = true
|
||||
|
||||
if (!Utils.isVisible()) {
|
||||
|
||||
// common flags
|
||||
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
|
||||
// message show
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.putExtra("action", "message show").putExtra("uap", uap)
|
||||
.putExtra("peer", peerUri)
|
||||
intent.putExtra("action", "message show").putExtra("uap", uap).putExtra("peer", peerUri)
|
||||
val pi = PendingIntent.getActivity(applicationContext, MESSAGE_REQ_CODE, intent, piFlags)
|
||||
|
||||
// message notification builder
|
||||
val senderDisplayName = Utils.friendlyUri(this, peerUri, ua.account)
|
||||
val senderPerson = Person.Builder()
|
||||
.setName(senderDisplayName)
|
||||
.setKey(peerUri)
|
||||
.build()
|
||||
val localUserPerson = Person.Builder()
|
||||
.setName(getString(R.string.you))
|
||||
.setKey(ua.account.aor)
|
||||
.build()
|
||||
val messagingStyle = MessagingStyle(localUserPerson)
|
||||
.setConversationTitle(null)
|
||||
.setGroupConversation(false)
|
||||
.addMessage(text, timeStamp, senderPerson)
|
||||
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
||||
val sender = Utils.friendlyUri(this, peerUri, ua.account)
|
||||
nb.setSmallIcon(R.drawable.ic_stat_message)
|
||||
.setSmallIcon(R.drawable.ic_stat_message)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorBaresip))
|
||||
.setContentIntent(pi)
|
||||
.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(getString(R.string.message_from) + " " + sender)
|
||||
.setContentText(text)
|
||||
val replyIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
replyIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
replyIntent.putExtra("action", "message reply")
|
||||
.putExtra("uap", uap).putExtra("peer", peerUri)
|
||||
val rpi = PendingIntent.getActivity(applicationContext, REPLY_REQ_CODE, replyIntent,
|
||||
piFlags)
|
||||
.setStyle(messagingStyle)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
// messafe inline reply
|
||||
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY)
|
||||
.setLabel(getString(R.string.reply))
|
||||
.build()
|
||||
val directReplyIntent = Intent(this, BaresipService::class.java)
|
||||
directReplyIntent.action = "Message Inline Reply"
|
||||
directReplyIntent.putExtra("uap", uap).putExtra("peer", peerUri).putExtra("time", timeStamp)
|
||||
val directReplyPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
DIRECT_REPLY_REQ_CODE,
|
||||
directReplyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||
)
|
||||
val inlineReplyAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_stat_reply,
|
||||
getString(R.string.reply),
|
||||
directReplyPendingIntent
|
||||
).addRemoteInput(remoteInput)
|
||||
.setAllowGeneratedReplies(true)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY).
|
||||
build()
|
||||
|
||||
// message save
|
||||
val saveIntent = Intent(this, BaresipService::class.java)
|
||||
saveIntent.action = "Message Save"
|
||||
saveIntent.putExtra("uap", uap)
|
||||
.putExtra("time", timeStampString)
|
||||
val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent,
|
||||
piFlags)
|
||||
saveIntent.putExtra("uap", uap).putExtra("time", timeStampString)
|
||||
val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags)
|
||||
val saveAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_stat_save,
|
||||
getString(R.string.save),
|
||||
savePendingIntent
|
||||
).build()
|
||||
|
||||
// message delete
|
||||
val deleteIntent = Intent(this, BaresipService::class.java)
|
||||
deleteIntent.action = "Message Delete"
|
||||
deleteIntent.putExtra("uap", uap)
|
||||
.putExtra("time", timeStampString)
|
||||
val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE,
|
||||
deleteIntent, piFlags)
|
||||
nb.addAction(R.drawable.ic_stat_reply, "Reply", rpi)
|
||||
nb.addAction(R.drawable.ic_stat_save, "Save", savePendingIntent)
|
||||
nb.addAction(R.drawable.ic_stat_delete, "Delete", deletePendingIntent)
|
||||
deleteIntent.putExtra("uap", uap).putExtra("time", timeStampString)
|
||||
val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags)
|
||||
val deleteAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_stat_delete,
|
||||
getString(R.string.delete),
|
||||
deletePendingIntent
|
||||
).build()
|
||||
|
||||
nb.addAction(inlineReplyAction).addAction(saveAction).addAction(deleteAction)
|
||||
nm.notify(MESSAGE_NOTIFICATION_ID, nb.build())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -1297,13 +1369,6 @@ class BaresipService: Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable {
|
||||
val spannable: Spannable = SpannableString(applicationContext.getText(stringRes))
|
||||
spannable.setSpan(
|
||||
ForegroundColorSpan(applicationContext.getColor(colorRes)), 0, spannable.length, 0)
|
||||
return spannable
|
||||
}
|
||||
|
||||
private fun startRinging() {
|
||||
am.mode = AudioManager.MODE_RINGTONE
|
||||
rt!!.isLooping = true
|
||||
@ -1638,12 +1703,9 @@ class BaresipService: Service() {
|
||||
val baresipContacts = mutableStateOf(emptyList<Contact.BaresipContact>())
|
||||
val androidContacts = mutableStateOf(emptyList<Contact.AndroidContact>())
|
||||
val contactNames = mutableStateOf(emptyList<String>())
|
||||
val contactUpdate = MutableLiveData<Long>()
|
||||
val darkTheme = mutableStateOf(false)
|
||||
var messages by mutableStateOf(emptyList<Message>())
|
||||
val messageUpdate = MutableLiveData<Long>()
|
||||
val chatTexts: MutableMap<String, String> = mutableMapOf()
|
||||
val activities = mutableListOf<String>()
|
||||
val registrationUpdate = MutableLiveData<Long>()
|
||||
val serviceEvent = MutableLiveData<Event<Long>>()
|
||||
val serviceEvents = mutableListOf<ServiceEvent>()
|
||||
@ -1661,12 +1723,25 @@ class BaresipService: Service() {
|
||||
private var aec: AcousticEchoCanceler? = null
|
||||
var agcAvailable = false
|
||||
var rt: Ringtone? = null
|
||||
|
||||
private var agc: AutomaticGainControl? = null
|
||||
private val nsAvailable = NoiseSuppressor.isAvailable()
|
||||
private var ns: NoiseSuppressor? = null
|
||||
private var btAdapter: BluetoothAdapter? = null
|
||||
private var recorderSessionId = 0
|
||||
|
||||
internal const val KEY_TEXT_REPLY = "key_text_reply_baresip"
|
||||
|
||||
fun postServiceEvent(event: ServiceEvent) {
|
||||
serviceEvents.add(event)
|
||||
if (serviceEvents.size == 1) {
|
||||
Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}")
|
||||
serviceEvent.postValue(Event(event.timeStamp))
|
||||
} else {
|
||||
Log.d(TAG, "Added service event ${event.event}")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAudioFocus(ctx: Context): Boolean {
|
||||
Log.d(TAG, "Requesting audio focus")
|
||||
if (audioFocusRequest != null) {
|
||||
|
||||
@ -1,404 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
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.fillMaxHeight
|
||||
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.safeDrawingPadding
|
||||
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.Surface
|
||||
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.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 com.tutpro.baresip.CallsActivity.Companion.uaHistory
|
||||
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
|
||||
|
||||
class CallDetailsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var aor: String
|
||||
private lateinit var peer: String
|
||||
private lateinit var account: Account
|
||||
private lateinit var callRow: CallRow
|
||||
private lateinit var details: ArrayList<CallRow. Details>
|
||||
private val decPlayer = MediaPlayer()
|
||||
private val encPlayer = MediaPlayer()
|
||||
private var position = 0
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
account = Account.ofAor(aor)!!
|
||||
peer = intent.getStringExtra("peer")!!
|
||||
position = intent.getIntExtra("position", 0)
|
||||
callRow = uaHistory.value[position]
|
||||
details = callRow.details
|
||||
|
||||
Utils.addActivity("call_details,$aor,$peer,$position")
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
CallDetailsScreen(this, stringResource(R.string.call_details)) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CallDetailsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
CallDetailsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallDetailsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
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, peer, account)
|
||||
Calls(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Peer(ctx: Context, peer: String, account: Account) {
|
||||
val headerText = stringResource(R.string.peer) + " " +
|
||||
Utils.friendlyUri(ctx, peer, account)
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Calls(ctx: Context) {
|
||||
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
|
||||
.imePadding()
|
||||
.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
|
||||
fun startTime(detail: CallRow.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
|
||||
fun Duration(ctx: Context, detail: CallRow.Details, durationText: String) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val recording = detail.recording
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
MainActivity.activityAor = aor
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("call_details,$aor,$peer,$position")
|
||||
decPlayer.stop()
|
||||
decPlayer.release()
|
||||
encPlayer.stop()
|
||||
encPlayer.release()
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
326
app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt
Normal file
326
app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,9 +5,7 @@ import java.util.*
|
||||
class CallRow(
|
||||
val aor: String, val peerUri: String, val direction: Int, startTime: GregorianCalendar?,
|
||||
val stopTime: GregorianCalendar, val recording: Array<String>
|
||||
)
|
||||
{
|
||||
|
||||
) {
|
||||
class Details(
|
||||
val direction: Int, val startTime: GregorianCalendar?,
|
||||
val stopTime: GregorianCalendar, val recording: Array<String>
|
||||
|
||||
@ -1,545 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.ImageAvatar
|
||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
class CallsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var account: Account
|
||||
|
||||
private var aor = ""
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
val title = getString(R.string.call_history)
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
Utils.addActivity("calls,$aor")
|
||||
|
||||
val ua = UserAgent.ofAor(aor)!!
|
||||
account = ua.account
|
||||
aorGenerateHistory(aor)
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
CallsScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
account.missedCalls = false
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(title, navigateBack) },
|
||||
content = { contentPadding ->
|
||||
CallsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(title: String, navigateBack: () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val delete = stringResource(R.string.delete)
|
||||
val disable = stringResource(R.string.disable_history)
|
||||
val enable = stringResource(R.string.enable_history)
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = String.format(stringResource(R.string.delete_history_alert), aor.substringAfter(":")),
|
||||
positiveButtonText = stringResource(R.string.delete),
|
||||
negativeButtonText = stringResource(R.string.cancel),
|
||||
onPositiveClicked = positiveAction.value,
|
||||
)
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
CustomElements.DropdownMenu(expanded,
|
||||
{ expanded = false },
|
||||
listOf(delete, if (account.callHistory) disable else enable),
|
||||
onItemClick = { selectedItem ->
|
||||
expanded = false
|
||||
when (selectedItem) {
|
||||
delete -> {
|
||||
positiveAction.value = {
|
||||
CallHistoryNew.clear(aor)
|
||||
uaHistory.value = emptyList()
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
disable, enable -> {
|
||||
account.callHistory = !account.callHistory
|
||||
AccountsActivity.saveAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Account(account)
|
||||
Calls(ctx, account)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Account(account: Account) {
|
||||
val headerText = stringResource(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
aor.split(":")[1]
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Calls(ctx: Context, account: Account) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val message = remember { mutableStateOf("") }
|
||||
val positiveButtonText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralButtonText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = message.value,
|
||||
positiveButtonText = positiveButtonText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralButtonText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(items = uaHistory.value) { callRow ->
|
||||
var recordings = false
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.combinedClickable(
|
||||
onClick = {
|
||||
val peerUri = callRow.peerUri
|
||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
||||
message.value = String.format(getString(R.string.contact_action_question), peerName)
|
||||
positiveButtonText.value = getString(R.string.call)
|
||||
positiveAction.value = {
|
||||
BaresipService.activities.remove("calls,$aor")
|
||||
MainActivity.activityAor = aor
|
||||
returnResult()
|
||||
val i = Intent(this@CallsActivity, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "call")
|
||||
i.putExtra("uap", UserAgent.ofAor(aor)!!.uap)
|
||||
i.putExtra("peer", peerUri)
|
||||
startActivity(i)
|
||||
}
|
||||
neutralButtonText.value = getString(R.string.send_message)
|
||||
neutralAction.value = {
|
||||
BaresipService.activities.remove("calls,$aor")
|
||||
MainActivity.activityAor = aor
|
||||
returnResult()
|
||||
val i = Intent(this@CallsActivity, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "message")
|
||||
i.putExtra("uap", UserAgent.ofAor(aor)!!.uap)
|
||||
i.putExtra("peer", peerUri)
|
||||
startActivity(i)
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
onLongClick = {
|
||||
val peerUri = callRow.peerUri
|
||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
||||
val callText: String = if (callRow.details.size > 1)
|
||||
getString(R.string.calls_calls)
|
||||
else
|
||||
getString(R.string.calls_call)
|
||||
val contactExists = Contact.nameExists(peerName, BaresipService.contacts, false)
|
||||
if (contactExists) {
|
||||
message.value = String.format(
|
||||
getString(R.string.calls_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
positiveButtonText.value = getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
removeFromHistory(callRow)
|
||||
}
|
||||
neutralButtonText.value = ""
|
||||
}
|
||||
else {
|
||||
message.value = String.format(
|
||||
getString(R.string.calls_add_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
positiveButtonText.value = getString(R.string.add_contact)
|
||||
positiveAction.value = {
|
||||
val i = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", callRow.peerUri)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i)
|
||||
}
|
||||
neutralButtonText.value = getString(R.string.delete)
|
||||
neutralAction.value = {
|
||||
removeFromHistory(callRow)
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
)
|
||||
) {
|
||||
val uri = callRow.peerUri
|
||||
when (val contact = Contact.findContact(uri)) {
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
ImageAvatar(avatarImage)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
null -> {
|
||||
val avatarImage = BitmapFactory
|
||||
.decodeResource(ctx.resources, R.drawable.person_image)
|
||||
ImageAvatar(avatarImage)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
var count = 1
|
||||
for (d in callRow.details) {
|
||||
if (d.recording[0] != "")
|
||||
recordings = true
|
||||
if (count > 3)
|
||||
continue
|
||||
Image(painterResource(d.direction), "Direction")
|
||||
count++
|
||||
}
|
||||
if (count > 3)
|
||||
Text("...", color = LocalCustomColors.current.itemText)
|
||||
Text(text = Utils.friendlyUri(ctx, callRow.peerUri, account),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(modifier = Modifier.width(56.dp)) {
|
||||
Text(
|
||||
text = Utils.relativeTime(ctx, callRow.stopTime),
|
||||
fontSize = 12.sp,
|
||||
minLines = 2, maxLines = 2,
|
||||
lineHeight = 16.sp,
|
||||
textAlign = TextAlign.End,
|
||||
color = if (recordings)
|
||||
LocalCustomColors.current.accent
|
||||
else
|
||||
LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.width(64.dp)
|
||||
.clickable(onClick = {
|
||||
val i = Intent(ctx, CallDetailsActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", account.aor)
|
||||
b.putString("peer", callRow.peerUri)
|
||||
b.putInt("position", uaHistory.value.indexOf(callRow))
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("calls,$aor")
|
||||
returnResult()
|
||||
}
|
||||
|
||||
private fun returnResult() {
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
MainActivity.activityAor = aor
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun aorGenerateHistory(aor: String) {
|
||||
uaHistory.value = emptyList()
|
||||
for (i in BaresipService.callHistory.indices.reversed()) {
|
||||
val h = BaresipService.callHistory[i]
|
||||
if (h.aor == aor) {
|
||||
val direction: Int = if (h.direction == "in") {
|
||||
if (h.startTime != null) {
|
||||
R.drawable.call_down_green
|
||||
} else {
|
||||
if (h.rejected)
|
||||
R.drawable.call_down_red
|
||||
else
|
||||
R.drawable.call_missed_in
|
||||
}
|
||||
} else {
|
||||
if (h.startTime != null) {
|
||||
R.drawable.call_up_green
|
||||
} else {
|
||||
if (h.rejected)
|
||||
R.drawable.call_up_red
|
||||
else
|
||||
R.drawable.call_missed_out
|
||||
}
|
||||
}
|
||||
if (uaHistory.value.isNotEmpty() && (uaHistory.value.last().peerUri == h.peerUri))
|
||||
uaHistory.value.last().details.add(CallRow.Details(
|
||||
direction, h.startTime,
|
||||
h.stopTime, h.recording
|
||||
))
|
||||
else
|
||||
addToUaHistory(
|
||||
CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromHistory(callRow: CallRow) {
|
||||
for (details in callRow.details) {
|
||||
if (details.recording[0] != "")
|
||||
CallHistoryNew.deleteRecording(details.recording)
|
||||
BaresipService.callHistory.removeAll {
|
||||
it.startTime == details.startTime && it.stopTime == details.stopTime
|
||||
}
|
||||
}
|
||||
CallHistoryNew.deleteRecording(callRow.recording)
|
||||
deleteFromUaHistory(callRow)
|
||||
CallHistoryNew.save()
|
||||
}
|
||||
|
||||
private fun addToUaHistory(callRow: CallRow) {
|
||||
val updatedList = uaHistory.value.toMutableList()
|
||||
updatedList.add(callRow)
|
||||
uaHistory.value = updatedList
|
||||
}
|
||||
|
||||
private fun deleteFromUaHistory(callRow: CallRow) {
|
||||
val updatedList = uaHistory.value.toMutableList()
|
||||
updatedList.remove(callRow)
|
||||
uaHistory.value = updatedList
|
||||
}
|
||||
|
||||
@SuppressLint("MutableCollectionMutableState")
|
||||
companion object {
|
||||
val uaHistory = mutableStateOf(emptyList<CallRow>())
|
||||
}
|
||||
|
||||
}
|
||||
483
app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt
Normal file
483
app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt
Normal file
@ -0,0 +1,483 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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
|
||||
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.size
|
||||
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.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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.text.style.TextOverflow
|
||||
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 com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
fun NavGraphBuilder.callsScreenRoute(navController: NavController, viewModel: ViewModel) {
|
||||
composable(
|
||||
route = "calls/{aor}",
|
||||
arguments = listOf(navArgument("aor") { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
val aor = backStackEntry.arguments?.getString("aor")!!
|
||||
CallsScreen(navController, viewModel, aor)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CallsScreen(navController: NavController, viewModel: ViewModel, aor: String) {
|
||||
|
||||
val account = Account.ofAor(aor)!!
|
||||
|
||||
val callHistory: MutableState<List<CallRow>> = remember { mutableStateOf(emptyList<CallRow>()) }
|
||||
|
||||
var isHistoryLoaded by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(aor) {
|
||||
callHistory.value = loadCallHistory(aor)
|
||||
isHistoryLoaded = true
|
||||
}
|
||||
|
||||
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(navController, account, callHistory)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
if (isHistoryLoaded)
|
||||
CallsContent(
|
||||
LocalContext.current,
|
||||
navController,
|
||||
viewModel,
|
||||
contentPadding,
|
||||
account,
|
||||
callHistory
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopAppBar(navController: NavController, account: Account, callHistory: MutableState<List<CallRow>>) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val delete = stringResource(R.string.delete)
|
||||
val disable = stringResource(R.string.disable_history)
|
||||
val enable = stringResource(R.string.enable_history)
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = String.format(stringResource(R.string.delete_history_alert), account.text()),
|
||||
positiveButtonText = stringResource(R.string.delete),
|
||||
negativeButtonText = stringResource(R.string.cancel),
|
||||
onPositiveClicked = positiveAction.value,
|
||||
)
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.call_history),
|
||||
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()
|
||||
account.missedCalls = false
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
CustomElements.DropdownMenu(
|
||||
expanded,
|
||||
{ expanded = false },
|
||||
listOf(delete, if (account.callHistory) disable else enable),
|
||||
onItemClick = { selectedItem ->
|
||||
expanded = false
|
||||
when (selectedItem) {
|
||||
delete -> {
|
||||
positiveAction.value = {
|
||||
CallHistoryNew.clear(account.aor)
|
||||
callHistory.value = emptyList()
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
disable, enable -> {
|
||||
account.callHistory = !account.callHistory
|
||||
Account.saveAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallsContent(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
viewModel: ViewModel,
|
||||
contentPadding: PaddingValues,
|
||||
account: Account,
|
||||
callHistory: MutableState<List<CallRow>>
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Account(account)
|
||||
Calls(ctx, navController, viewModel, account, callHistory)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Account(account: Account) {
|
||||
val headerText = stringResource(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
account.aor.split(":")[1]
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun Calls(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
viewModel: ViewModel,
|
||||
account: Account,
|
||||
callHistory: MutableState<List<CallRow>>
|
||||
) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val message = remember { mutableStateOf("") }
|
||||
val positiveButtonText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralButtonText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = message.value,
|
||||
positiveButtonText = positiveButtonText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralButtonText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(items = callHistory.value, key = { callRow -> callRow.stopTime }) { callRow ->
|
||||
var recordings = false
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.combinedClickable(
|
||||
onClick = {
|
||||
val aor = account.aor
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
val peerUri = callRow.peerUri
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
if (ua != null) {
|
||||
intent.putExtra("uap", ua.uap)
|
||||
intent.putExtra("peer", peerUri)
|
||||
}
|
||||
else
|
||||
Log.w(TAG, "onClickListener did not find UA for $aor")
|
||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
||||
message.value = String.format(ctx.getString(R.string.contact_action_question), peerName)
|
||||
positiveButtonText.value = ctx.getString(R.string.call)
|
||||
positiveAction.value = {
|
||||
if (ua != null) {
|
||||
handleIntent(ctx, viewModel, intent, "call")
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
neutralButtonText.value = ctx.getString(R.string.send_message)
|
||||
neutralAction.value = {
|
||||
if (ua != null) {
|
||||
handleIntent(ctx, viewModel, intent, "message")
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
onLongClick = {
|
||||
val peerUri = callRow.peerUri
|
||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
||||
val callText: String = if (callRow.details.size > 1)
|
||||
ctx.getString(R.string.calls_calls)
|
||||
else
|
||||
ctx.getString(R.string.calls_call)
|
||||
val contactExists = Contact.nameExists(peerName, BaresipService.contacts, false)
|
||||
if (contactExists) {
|
||||
message.value = String.format(
|
||||
ctx.getString(R.string.calls_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
positiveButtonText.value = ctx.getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
removeFromHistory(callHistory, callRow)
|
||||
}
|
||||
neutralButtonText.value = ""
|
||||
}
|
||||
else {
|
||||
message.value = String.format(
|
||||
ctx.getString(R.string.calls_add_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
positiveButtonText.value = ctx.getString(R.string.add_contact)
|
||||
positiveAction.value = {
|
||||
navController.navigate("baresip_contact/${callRow.peerUri}/new")
|
||||
}
|
||||
neutralButtonText.value = ctx.getString(R.string.delete)
|
||||
neutralAction.value = {
|
||||
removeFromHistory(callHistory, callRow)
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
)
|
||||
) {
|
||||
val uri = callRow.peerUri
|
||||
when (val contact = Contact.findContact(uri)) {
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
CustomElements.ImageAvatar(avatarImage)
|
||||
else
|
||||
CustomElements.TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
CustomElements.TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
null -> {
|
||||
val avatarImage = BitmapFactory
|
||||
.decodeResource(ctx.resources, R.drawable.person_image)
|
||||
CustomElements.ImageAvatar(avatarImage)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
var count = 1
|
||||
for (d in callRow.details) {
|
||||
if (d.recording[0] != "")
|
||||
recordings = true
|
||||
if (count > 3)
|
||||
continue
|
||||
Image(painterResource(d.direction), "Direction")
|
||||
count++
|
||||
}
|
||||
if (count > 3)
|
||||
Text("...", color = LocalCustomColors.current.itemText)
|
||||
Text(text = Utils.friendlyUri(ctx, callRow.peerUri, account),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(modifier = Modifier.width(56.dp)) {
|
||||
Text(
|
||||
text = Utils.relativeTime(ctx, callRow.stopTime),
|
||||
fontSize = 12.sp,
|
||||
minLines = 2, maxLines = 2,
|
||||
lineHeight = 16.sp,
|
||||
textAlign = TextAlign.End,
|
||||
color = if (recordings)
|
||||
LocalCustomColors.current.accent
|
||||
else
|
||||
LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.width(64.dp)
|
||||
.clickable(onClick = {
|
||||
viewModel.selectCallRow(callRow)
|
||||
navController.navigate("call_details")
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCallHistory(aor: String): MutableList<CallRow> {
|
||||
val res = mutableListOf<CallRow>()
|
||||
for (i in BaresipService.callHistory.indices.reversed()) {
|
||||
val h = BaresipService.callHistory[i]
|
||||
if (h.aor == aor) {
|
||||
val direction: Int = if (h.direction == "in") {
|
||||
if (h.startTime != null) {
|
||||
R.drawable.call_down_green
|
||||
} else {
|
||||
if (h.rejected)
|
||||
R.drawable.call_down_red
|
||||
else
|
||||
R.drawable.call_missed_in
|
||||
}
|
||||
} else {
|
||||
if (h.startTime != null) {
|
||||
R.drawable.call_up_green
|
||||
} else {
|
||||
if (h.rejected)
|
||||
R.drawable.call_up_red
|
||||
else
|
||||
R.drawable.call_missed_out
|
||||
}
|
||||
}
|
||||
if (res.isNotEmpty() && res.last().peerUri == h.peerUri)
|
||||
res.last().details.add(CallRow.Details(
|
||||
direction, h.startTime,
|
||||
h.stopTime, h.recording
|
||||
))
|
||||
else
|
||||
res.add(CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private fun removeFromHistory(callHistory: MutableState<List<CallRow>>, callRow: CallRow) {
|
||||
for (details in callRow.details) {
|
||||
if (details.recording[0] != "")
|
||||
CallHistoryNew.deleteRecording(details.recording)
|
||||
BaresipService.callHistory.removeAll {
|
||||
it.startTime == details.startTime && it.stopTime == details.stopTime
|
||||
}
|
||||
}
|
||||
CallHistoryNew.deleteRecording(callRow.recording)
|
||||
val updatedList = callHistory.value.filterNot { it == callRow }
|
||||
callHistory.value = updatedList
|
||||
CallHistoryNew.save()
|
||||
}
|
||||
|
||||
@ -1,637 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.text.format.DateUtils.isToday
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.EventListener
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
class ChatActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var imm: InputMethodManager
|
||||
private lateinit var aor: String
|
||||
private lateinit var account: Account
|
||||
private lateinit var peerUri: String
|
||||
private lateinit var ua: UserAgent
|
||||
|
||||
private var _chatMessages = mutableStateOf<List<Message>>(emptyList())
|
||||
private var chatMessages : List<Message> by _chatMessages
|
||||
private var focus = false
|
||||
private var lastCall: Long = 0
|
||||
private var keyboardController: SoftwareKeyboardController? = null
|
||||
private val chatPeer = mutableStateOf("")
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
peerUri = intent.getStringExtra("peer")!!
|
||||
account = UserAgent.ofAor(aor)!!.account
|
||||
focus = intent.getBooleanExtra("focus", false)
|
||||
|
||||
if (BaresipService.activities.first().startsWith("chat,$aor,$peerUri")) {
|
||||
returnResult(RESULT_CANCELED)
|
||||
return
|
||||
} else {
|
||||
Utils.addActivity("chat,$aor,$peerUri,$focus")
|
||||
}
|
||||
|
||||
val userAgent = UserAgent.ofAor(aor)
|
||||
if (userAgent == null) {
|
||||
Log.w(TAG, "ChatActivity did not find ua of $aor")
|
||||
MainActivity.activityAor = aor
|
||||
returnResult(RESULT_CANCELED)
|
||||
return
|
||||
} else {
|
||||
ua = userAgent
|
||||
}
|
||||
|
||||
chatPeer.value = Utils.friendlyUri(this, peerUri, userAgent.account, true)
|
||||
|
||||
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
val messagesObserver = Observer<Long> {
|
||||
_chatMessages.value = listOf()
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
BaresipService.messageUpdate.observe(this, messagesObserver)
|
||||
|
||||
ua.account.unreadMessages = false
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
keyboardController = LocalSoftwareKeyboardController.current
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
ChatScreen(this) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatScreen(ctx: Context, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(ctx, navigateBack) },
|
||||
bottomBar = { NewMessage(ctx, peerUri) },
|
||||
content = { contentPadding ->
|
||||
ChatContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(ctx: Context, navigateBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = String.format(getString(R.string.chat_with), chatPeer.value),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastCall > 1000) {
|
||||
lastCall = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
intent.putExtra("action", "call")
|
||||
intent.putExtra("uap", ua.uap)
|
||||
intent.putExtra("peer", peerUri)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.call_small),
|
||||
contentDescription = "Call",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
Account(account)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Messages(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Account(account: Account) {
|
||||
val headerText = getString(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
aor.split(":")[1]
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Messages(ctx: Context) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
val positiveButtonText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralButtonText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = positiveButtonText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralButtonText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
|
||||
LaunchedEffect(chatMessages) {
|
||||
// Scroll to the bottom when new messages are added
|
||||
if (chatMessages.isNotEmpty()) {
|
||||
coroutineScope.launch {
|
||||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 2.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
reverseLayout = true,
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items = chatMessages, key = { message -> message.timeStamp }) { message ->
|
||||
val down = message.direction == MESSAGE_DOWN
|
||||
val peer: String = if (down) {
|
||||
if (chatPeer.value.startsWith("sip:") &&
|
||||
(Utils.uriHostPart(message.peerUri) == Utils.uriHostPart(message.aor)))
|
||||
Utils.uriUserPart(message.peerUri)
|
||||
else
|
||||
chatPeer.value
|
||||
}
|
||||
else
|
||||
stringResource(R.string.you)
|
||||
var info: String
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
info = fmt.format(cal.time)
|
||||
if (info.length < 6) info = "${stringResource(R.string.today)} $info"
|
||||
if (message.direction == MESSAGE_UP_FAIL) {
|
||||
info = if (message.responseCode != 0)
|
||||
"$info - ${stringResource(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
|
||||
else
|
||||
"$info - ${stringResource(R.string.sending_failed)}"
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(end = 12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (Contact.findContact(peerUri) == null) {
|
||||
dialogMessage.value = String.format(getString(R.string.long_message_question),
|
||||
chatPeer.value
|
||||
)
|
||||
positiveButtonText.value = getString(R.string.add_contact)
|
||||
positiveAction.value = {
|
||||
val i = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", peerUri)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i)
|
||||
}
|
||||
neutralButtonText.value = getString(R.string.delete)
|
||||
neutralAction.value = {
|
||||
message.delete()
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
}
|
||||
else {
|
||||
dialogMessage.value = getString(R.string.short_message_question)
|
||||
positiveButtonText.value = getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
message.delete()
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
neutralButtonText.value = ""
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
shape = if (message.direction == MESSAGE_DOWN)
|
||||
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
|
||||
else
|
||||
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryDark
|
||||
else
|
||||
LocalCustomColors.current.secondaryLight
|
||||
}
|
||||
else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryDark
|
||||
else
|
||||
LocalCustomColors.current.primaryLight
|
||||
}),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(
|
||||
start = if (message.direction == MESSAGE_DOWN) 0.dp else 24.dp,
|
||||
end = if (message.direction == MESSAGE_DOWN) 24.dp else 0.dp
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
Row {
|
||||
val textColor =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryLight
|
||||
else
|
||||
LocalCustomColors.current.secondaryDark
|
||||
}
|
||||
else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryLight
|
||||
else
|
||||
LocalCustomColors.current.primaryDark
|
||||
}
|
||||
Text(text = peer, fontSize = 12.sp, color = textColor)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = info, fontSize = 12.sp, color = textColor)
|
||||
}
|
||||
Row {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = message.message,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
|
||||
FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NewMessage(ctx: Context, peerUri: String) {
|
||||
|
||||
var newMessage = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(""))
|
||||
}
|
||||
var textFieldLoaded by remember { mutableStateOf(false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
EventListener {
|
||||
when (it) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
val chatText = BaresipService.chatTexts["$aor::$peerUri"]
|
||||
if (chatText != null) {
|
||||
Log.d(TAG, "Restoring newMessage '$chatText' for $aor::$peerUri")
|
||||
newMessage.value = TextFieldValue(
|
||||
text = chatText,
|
||||
selection = TextRange(chatText.length)
|
||||
)
|
||||
BaresipService.chatTexts.remove("$aor::$peerUri")
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
if (newMessage.value != TextFieldValue("")) {
|
||||
Log.d(TAG, "Saving newMessage '${newMessage.value.text}' for $aor::$peerUri")
|
||||
BaresipService.chatTexts["$aor::$peerUri"] = newMessage.value.text
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.notice),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
OutlinedTextField(
|
||||
value = newMessage.value,
|
||||
placeholder = { Text(stringResource(R.string.new_message)) },
|
||||
onValueChange = { newMessage.value = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
if (!textFieldLoaded)
|
||||
textFieldLoaded = true
|
||||
},
|
||||
singleLine = false,
|
||||
trailingIcon = {
|
||||
if (newMessage.value.text.isNotEmpty()) {
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.clickable { newMessage.value = TextFieldValue("") }
|
||||
)
|
||||
} },
|
||||
label = { LabelText(stringResource(R.string.new_message)) },
|
||||
textStyle = TextStyle(fontSize = 18.sp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
keyboardType = KeyboardType.Text,
|
||||
autoCorrectEnabled = true
|
||||
)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
if (newMessage.value.text.isNotEmpty())
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.send),
|
||||
contentDescription = "Send",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable {
|
||||
val msgText = newMessage.value.text
|
||||
if (msgText.isNotEmpty()) {
|
||||
keyboardController?.hide()
|
||||
val time = System.currentTimeMillis()
|
||||
val msg =
|
||||
Message(aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true)
|
||||
msg.add()
|
||||
var msgUri = ""
|
||||
_chatMessages.value += msg
|
||||
if (Utils.isTelUri(peerUri))
|
||||
if (ua.account.telProvider == "") {
|
||||
dialogMessage.value = String.format(
|
||||
getString(R.string.no_telephony_provider),
|
||||
Utils.plainAor(aor)
|
||||
)
|
||||
showDialog.value = true
|
||||
} else {
|
||||
msgUri = Utils.telToSip(peerUri, ua.account)
|
||||
}
|
||||
else
|
||||
msgUri = peerUri
|
||||
if (msgUri != "")
|
||||
if (Api.message_send(ua.uap, msgUri, msgText, time.toString()) != 0
|
||||
) {
|
||||
Toast.makeText(
|
||||
ctx, "${getString(R.string.message_failed)}!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
msg.direction = MESSAGE_UP_FAIL
|
||||
msg.responseReason = getString(R.string.message_failed)
|
||||
} else {
|
||||
newMessage.value = TextFieldValue("")
|
||||
keyboardController?.hide()
|
||||
BaresipService.chatTexts.remove("$aor::$peerUri")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
MainActivity.activityAor = aor
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
chatPeer.value = Utils.friendlyUri(this, peerUri, ua.account, true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
var save = false
|
||||
for (m in chatMessages) {
|
||||
if (m.new) {
|
||||
m.new = false
|
||||
save = true
|
||||
}
|
||||
}
|
||||
if (save) Message.save()
|
||||
keyboardController?.hide()
|
||||
BaresipService.activities.remove("chat,$aor,$peerUri,false")
|
||||
BaresipService.activities.remove("chat,$aor,$peerUri,true")
|
||||
returnResult(RESULT_OK)
|
||||
}
|
||||
|
||||
private fun returnResult(code: Int) {
|
||||
setResult(code, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun uaPeerMessages(aor: String, peerUri: String): List<Message> {
|
||||
val res = mutableListOf<Message>()
|
||||
for (m in BaresipService.messages.reversed())
|
||||
if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m)
|
||||
return res
|
||||
}
|
||||
|
||||
}
|
||||
580
app/src/main/kotlin/com/tutpro/baresip/ChatScreen.kt
Normal file
580
app/src/main/kotlin/com/tutpro/baresip/ChatScreen.kt
Normal file
@ -0,0 +1,580 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.format.DateUtils.isToday
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.String.format
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
fun NavGraphBuilder.chatScreenRoute(navController: NavController, viewModel: ViewModel) {
|
||||
composable(
|
||||
route = "chat/{aor}/{peer}",
|
||||
arguments = listOf(
|
||||
navArgument("aor") { type = NavType.StringType },
|
||||
navArgument("peer") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val aor = backStackEntry.arguments?.getString("aor")!!
|
||||
val peerUri = backStackEntry.arguments?.getString("peer")!!
|
||||
ChatScreen(
|
||||
ctx = LocalContext.current,
|
||||
navController = navController,
|
||||
viewModel = viewModel,
|
||||
account = Account.ofAor(aor)!!,
|
||||
peerUri = peerUri
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatScreen(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
viewModel: ViewModel,
|
||||
account: Account,
|
||||
peerUri: String
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val aor = account.aor
|
||||
|
||||
var chatMessages by remember(aor, peerUri) { mutableStateOf<List<Message>>(emptyList()) }
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
Log.d(TAG, "Resumed to ChatScreen for AOR: $aor peer $peerUri")
|
||||
chatMessages = loadPeerMessages(aor, peerUri)
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
var areMessagesLoaded by remember(aor, peerUri) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val reloadMessages = {
|
||||
Log.d(TAG, "Reloading messages for $aor peer $peerUri")
|
||||
chatMessages = loadPeerMessages(aor, peerUri)
|
||||
if (!areMessagesLoaded)
|
||||
areMessagesLoaded = true
|
||||
}
|
||||
|
||||
val addMessage = { newMessage: Message ->
|
||||
chatMessages = chatMessages + newMessage
|
||||
}
|
||||
|
||||
DisposableEffect(key1 = lifecycleOwner, key2 = account.aor, key3 = peerUri) {
|
||||
val messagesObserver = Observer<Long> { timestamp ->
|
||||
Log.d(TAG, "Message update received via LiveData for $peerUri, timestamp: $timestamp")
|
||||
reloadMessages()
|
||||
}
|
||||
reloadMessages() // Initial load
|
||||
Log.d(TAG, "Observing message updates for $peerUri")
|
||||
BaresipService.messageUpdate.observe(lifecycleOwner, messagesObserver)
|
||||
onDispose {
|
||||
Log.d(TAG, "Removing message observer for $peerUri")
|
||||
BaresipService.messageUpdate.removeObserver(messagesObserver)
|
||||
}
|
||||
}
|
||||
|
||||
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(ctx, navController, viewModel, account, peerUri)
|
||||
}
|
||||
},
|
||||
bottomBar = { NewMessage(ctx, account, peerUri, addMessage) },
|
||||
content = { contentPadding ->
|
||||
if (areMessagesLoaded)
|
||||
ChatContent(ctx, navController, contentPadding, account, peerUri, chatMessages, reloadMessages)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopAppBar(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
viewModel: ViewModel,
|
||||
account: Account,
|
||||
peerUri: String
|
||||
) {
|
||||
val aor = account.aor
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = format(ctx.getString(R.string.chat_with), Utils.friendlyUri(ctx, peerUri, account)),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
Message.updateMessagesFromPearRead(aor, peerUri)
|
||||
account.unreadMessages = Message.unreadMessages(aor)
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
val ua = UserAgent.ofAor(account.aor)
|
||||
if (ua != null) {
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
intent.putExtra("uap", ua.uap)
|
||||
intent.putExtra("peer", peerUri)
|
||||
handleIntent(ctx, viewModel, intent, "call")
|
||||
navController.navigate("main") {
|
||||
popUpTo("main")
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
else
|
||||
Log.w(TAG, "onClickListener did not find UA for $aor")
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.call_small),
|
||||
contentDescription = "Call",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatContent(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
contentPadding: PaddingValues,
|
||||
account: Account,
|
||||
peerUri: String,
|
||||
messages: List<Message>,
|
||||
onMessageDeleted: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(contentPadding),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
Account(ctx, account)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Messages(ctx, navController, account, peerUri, messages, onMessageDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Account(ctx: Context, account: Account) {
|
||||
val headerText = ctx.getString(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
account.aor.substringAfter(":")
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Messages(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
account: Account,
|
||||
peerUri: String,
|
||||
messages: List<Message>,
|
||||
onMessageDeleted: () -> Unit
|
||||
) {
|
||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
val positiveButtonText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralButtonText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = positiveButtonText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralButtonText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(messages) {
|
||||
// Scroll to the bottom when new messages are added
|
||||
if (messages.isNotEmpty()) {
|
||||
coroutineScope.launch {
|
||||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 2.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
reverseLayout = true,
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items = messages, key = { message -> message.timeStamp }) { message ->
|
||||
val down = message.direction == MESSAGE_DOWN
|
||||
val sender: String = if (down)
|
||||
peerName
|
||||
else if (BaresipService.uas.value.size == 1)
|
||||
stringResource(R.string.you)
|
||||
else
|
||||
account.text()
|
||||
var info: String
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
info = fmt.format(cal.time)
|
||||
if (info.length < 6) info = "${stringResource(R.string.today)} $info"
|
||||
if (message.direction == MESSAGE_UP_FAIL) {
|
||||
info = if (message.responseCode != 0)
|
||||
"$info - ${stringResource(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
|
||||
else
|
||||
"$info - ${stringResource(R.string.sending_failed)}"
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(end = 12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (Contact.findContact(peerUri) == null) {
|
||||
dialogMessage.value = String.format(
|
||||
ctx.getString(R.string.long_message_question),
|
||||
peerUri
|
||||
)
|
||||
positiveButtonText.value = ctx.getString(R.string.add_contact)
|
||||
positiveAction.value = {
|
||||
navController.navigate("baresip_contact/$peerUri/new")
|
||||
}
|
||||
neutralButtonText.value = ctx.getString(R.string.delete)
|
||||
neutralAction.value = {
|
||||
message.delete()
|
||||
onMessageDeleted
|
||||
}
|
||||
} else {
|
||||
dialogMessage.value = ctx.getString(R.string.short_message_question)
|
||||
positiveButtonText.value = ctx.getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
message.delete()
|
||||
onMessageDeleted()
|
||||
}
|
||||
neutralButtonText.value = ""
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
shape = if (message.direction == MESSAGE_DOWN)
|
||||
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
|
||||
else
|
||||
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryDark
|
||||
else
|
||||
LocalCustomColors.current.secondaryLight
|
||||
} else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryDark
|
||||
else
|
||||
LocalCustomColors.current.primaryLight
|
||||
}
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(
|
||||
start = if (message.direction == MESSAGE_DOWN) 0.dp else 24.dp,
|
||||
end = if (message.direction == MESSAGE_DOWN) 24.dp else 0.dp
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
Row {
|
||||
val textColor =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryLight
|
||||
else
|
||||
LocalCustomColors.current.secondaryDark
|
||||
} else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryLight
|
||||
else
|
||||
LocalCustomColors.current.primaryDark
|
||||
}
|
||||
Text(text = sender, fontSize = 12.sp, color = textColor)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = info, fontSize = 12.sp, color = textColor)
|
||||
}
|
||||
Row {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = message.message,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
|
||||
FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun NewMessage(
|
||||
ctx: Context,
|
||||
account: Account,
|
||||
peerUri: String,
|
||||
addMessage: (message: Message) -> Unit
|
||||
) {
|
||||
|
||||
val aor = account.aor
|
||||
val ua = UserAgent.ofAor(aor)!!
|
||||
|
||||
var newMessage = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(""))
|
||||
}
|
||||
var textFieldLoaded by remember { mutableStateOf(false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.notice),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
OutlinedTextField(
|
||||
value = newMessage.value,
|
||||
placeholder = { Text(stringResource(R.string.new_message)) },
|
||||
onValueChange = { newMessage.value = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
if (!textFieldLoaded)
|
||||
textFieldLoaded = true
|
||||
},
|
||||
singleLine = false,
|
||||
trailingIcon = {
|
||||
if (newMessage.value.text.isNotEmpty()) {
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.clickable { newMessage.value = TextFieldValue("") }
|
||||
)
|
||||
} },
|
||||
label = { LabelText(stringResource(R.string.new_message)) },
|
||||
textStyle = TextStyle(fontSize = 18.sp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
keyboardType = KeyboardType.Text,
|
||||
autoCorrectEnabled = true
|
||||
)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
if (newMessage.value.text.isNotEmpty())
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.send),
|
||||
contentDescription = "Send",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable {
|
||||
val msgText = newMessage.value.text
|
||||
if (msgText.isNotEmpty()) {
|
||||
keyboardController?.hide()
|
||||
val time = System.currentTimeMillis()
|
||||
val msg =
|
||||
Message(aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true)
|
||||
msg.add()
|
||||
var msgUri = ""
|
||||
addMessage(msg)
|
||||
if (Utils.isTelUri(peerUri))
|
||||
if (ua.account.telProvider == "") {
|
||||
dialogMessage.value = String.format(
|
||||
ctx.getString(R.string.no_telephony_provider),
|
||||
Utils.plainAor(aor)
|
||||
)
|
||||
showDialog.value = true
|
||||
} else {
|
||||
msgUri = Utils.telToSip(peerUri, ua.account)
|
||||
}
|
||||
else
|
||||
msgUri = peerUri
|
||||
if (msgUri != "")
|
||||
if (Api.message_send(ua.uap, msgUri, msgText, time.toString()) != 0
|
||||
) {
|
||||
Toast.makeText(
|
||||
ctx, "${ctx.getString(R.string.message_failed)}!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
msg.direction = MESSAGE_UP_FAIL
|
||||
msg.responseReason = ctx.getString(R.string.message_failed)
|
||||
} else {
|
||||
newMessage.value = TextFieldValue("")
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPeerMessages(aor: String, peerUri: String): List<Message> {
|
||||
val res = mutableListOf<Message>()
|
||||
for (m in BaresipService.messages.reversed())
|
||||
if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m)
|
||||
return res
|
||||
}
|
||||
@ -1,728 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils.isToday
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.tutpro.baresip.BaresipService.Companion.contactNames
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.ImageAvatar
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
class ChatsActivity: ComponentActivity() {
|
||||
|
||||
internal lateinit var aor: String
|
||||
internal lateinit var account: Account
|
||||
private lateinit var chatRequest: ActivityResultLauncher<Intent>
|
||||
|
||||
private var _uaMessages = mutableStateOf<List<Message>>(emptyList())
|
||||
private var uaMessages: List<Message> by _uaMessages
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
aor = intent.extras!!.getString("aor")!!
|
||||
Utils.addActivity("chats,$aor")
|
||||
account = UserAgent.ofAor(aor)!!.account
|
||||
|
||||
val title = getString(R.string.chats)
|
||||
|
||||
_uaMessages.value = uaMessages(aor)
|
||||
|
||||
chatRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK)
|
||||
_uaMessages.value = uaMessages(aor)
|
||||
}
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ChatsScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(title, navigateBack) },
|
||||
bottomBar = { NewChatPeer() },
|
||||
content = { contentPadding ->
|
||||
ChatsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(title: String, navigateBack: () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val delete = stringResource(R.string.delete)
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = String.format(
|
||||
stringResource(R.string.delete_chats_alert),
|
||||
aor.substringAfter(":")
|
||||
),
|
||||
positiveButtonText = stringResource(R.string.delete),
|
||||
onPositiveClicked = positiveAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel),
|
||||
)
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
CustomElements.DropdownMenu(
|
||||
expanded,
|
||||
{ expanded = false },
|
||||
listOf(delete),
|
||||
onItemClick = { selectedItem ->
|
||||
expanded = false
|
||||
when (selectedItem) {
|
||||
delete -> {
|
||||
positiveAction.value = {
|
||||
Message.clearMessagesOfAor(aor)
|
||||
Message.save()
|
||||
_uaMessages.value = listOf()
|
||||
account.unreadMessages = false
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Account(account)
|
||||
Chats(ctx, account)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Account(account: Account) {
|
||||
val headerText = stringResource(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
aor.split(":")[1]
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Chats(ctx: Context, account: Account) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
val positiveButtonText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralButtonText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
if (showDialog.value)
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = positiveButtonText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralButtonText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp, end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
reverseLayout = true,
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items = uaMessages, key = { message -> message.timeStamp }) { message ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
when (val contact = Contact.findContact(message.peerUri)) {
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
ImageAvatar(avatarImage)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
|
||||
null -> {
|
||||
val avatarImage = BitmapFactory
|
||||
.decodeResource(ctx.resources, R.drawable.person_image)
|
||||
ImageAvatar(avatarImage)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
CustomElements.Button(
|
||||
onClick = {
|
||||
val i = Intent(ctx, ChatActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("peer", message.peerUri)
|
||||
i.putExtras(b)
|
||||
chatRequest.launch(i)
|
||||
},
|
||||
onLongClick = {
|
||||
val peer = Utils.friendlyUri(ctx, message.peerUri, account)
|
||||
val contactExists =
|
||||
Contact.nameExists(peer, BaresipService.contacts, false)
|
||||
if (contactExists) {
|
||||
dialogMessage.value = String.format(
|
||||
getString(R.string.short_chat_question),
|
||||
peer
|
||||
)
|
||||
positiveButtonText.value = getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
Message.deleteAorPeerMessages(aor, message.peerUri)
|
||||
_uaMessages.value = uaMessages(aor)
|
||||
}
|
||||
neutralButtonText.value = ""
|
||||
} else {
|
||||
dialogMessage.value = String.format(
|
||||
getString(R.string.long_chat_question),
|
||||
peer
|
||||
)
|
||||
positiveButtonText.value = getString(R.string.add_contact)
|
||||
positiveAction.value = {
|
||||
val i = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", message.peerUri)
|
||||
i.putExtras(b)
|
||||
startActivity(i)
|
||||
}
|
||||
neutralButtonText.value = getString(R.string.delete)
|
||||
neutralAction.value = {
|
||||
Message.deleteAorPeerMessages(aor, message.peerUri)
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(end = 6.dp),
|
||||
shape = if (message.direction == MESSAGE_DOWN)
|
||||
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
|
||||
else
|
||||
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
|
||||
color =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryDark
|
||||
else
|
||||
LocalCustomColors.current.secondaryLight
|
||||
} else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryDark
|
||||
else
|
||||
LocalCustomColors.current.primaryLight
|
||||
},
|
||||
) {
|
||||
val peer = Utils.friendlyUri(ctx, message.peerUri, account)
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
val info = fmt.format(cal.time)
|
||||
Column {
|
||||
Row {
|
||||
val textColor =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryLight
|
||||
else
|
||||
LocalCustomColors.current.secondaryDark
|
||||
} else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryLight
|
||||
else
|
||||
LocalCustomColors.current.primaryDark
|
||||
}
|
||||
Text(text = peer, color = textColor, fontSize = 12.sp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = info, color = textColor, fontSize = 12.sp)
|
||||
}
|
||||
Row {
|
||||
BasicText(
|
||||
text = message.message,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
|
||||
FontWeight.Bold else FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewChatPeer() {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val items = remember { mutableStateOf(listOf<String>()) }
|
||||
val itemAction = remember { mutableStateOf<(Int) -> Unit>({ _ -> run {} }) }
|
||||
|
||||
SelectableAlertDialog(
|
||||
openDialog = showDialog,
|
||||
title = stringResource(R.string.choose_destination_uri),
|
||||
items = items.value,
|
||||
onItemClicked = itemAction.value,
|
||||
neutralButtonText = stringResource(R.string.cancel),
|
||||
onNeutralClicked = {}
|
||||
)
|
||||
|
||||
val suggestions by remember { contactNames }
|
||||
var filteredSuggestions by remember { mutableStateOf(suggestions) }
|
||||
var showSuggestions by remember { mutableStateOf(false) }
|
||||
val lazyListState = rememberLazyListState()
|
||||
val focusManager = LocalFocusManager.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var newPeer by remember { mutableStateOf("") }
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (showSuggestions && filteredSuggestions.isNotEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.shadow(8.dp, RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
LocalCustomColors.current.grayLight,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.animateContentSize()
|
||||
) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 150.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 180.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
state = lazyListState
|
||||
) {
|
||||
items(
|
||||
items = filteredSuggestions,
|
||||
key = { suggestion -> suggestion }
|
||||
) { suggestion ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
newPeer = suggestion
|
||||
showSuggestions = false
|
||||
}
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = suggestion,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = LocalCustomColors.current.grayDark,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = newPeer,
|
||||
placeholder = {
|
||||
Text(stringResource(R.string.new_chat_peer))
|
||||
},
|
||||
onValueChange = {
|
||||
newPeer = it
|
||||
showSuggestions = newPeer.length > 2
|
||||
filteredSuggestions = if (it.isEmpty()) {
|
||||
suggestions
|
||||
} else {
|
||||
suggestions.filter { suggestion ->
|
||||
newPeer.length > 2 && suggestion.startsWith(
|
||||
newPeer,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 6.dp)
|
||||
.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (newPeer.isNotEmpty())
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
if (showSuggestions)
|
||||
showSuggestions = false
|
||||
else
|
||||
newPeer = ""
|
||||
}
|
||||
)
|
||||
},
|
||||
label = {
|
||||
LabelText(stringResource(R.string.new_chat_peer))
|
||||
},
|
||||
textStyle = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = LocalCustomColors.current.itemText
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Done
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
SmallFloatingActionButton(
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
onClick = {
|
||||
showSuggestions = false
|
||||
val peerText = newPeer.trim()
|
||||
if (peerText.isNotEmpty()) {
|
||||
val uris = Contact.contactUris(peerText)
|
||||
if (uris.isEmpty())
|
||||
makeChat(peerText)
|
||||
else if (uris.size == 1)
|
||||
makeChat(uris[0])
|
||||
else {
|
||||
items.value = uris
|
||||
itemAction.value = { index ->
|
||||
makeChat(uris[index])
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
}
|
||||
newPeer = ""
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
containerColor = LocalCustomColors.current.accent,
|
||||
contentColor = LocalCustomColors.current.background
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
modifier = Modifier.size(36.dp),
|
||||
contentDescription = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeChat(chatPeer: String) {
|
||||
val peerUri = if (Utils.isTelNumber(chatPeer))
|
||||
"tel:$chatPeer"
|
||||
else
|
||||
chatPeer
|
||||
val uri = if (Utils.isTelUri(peerUri)) {
|
||||
if (account.telProvider == "") {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value =
|
||||
String.format(getString(R.string.no_telephony_provider), account.aor)
|
||||
showAlert.value = true
|
||||
""
|
||||
} else
|
||||
Utils.telToSip(peerUri, account)
|
||||
} else
|
||||
Utils.uriComplete(peerUri, aor)
|
||||
if (alertMessage.value.isEmpty()) {
|
||||
if (!Utils.checkUri(uri)) {
|
||||
alertTitle.value = getString(R.string.notice)
|
||||
alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), uri)
|
||||
showAlert.value = true
|
||||
}
|
||||
else {
|
||||
val i = Intent(this@ChatsActivity, ChatActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("peer", uri)
|
||||
i.putExtras(b)
|
||||
chatRequest.launch(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
MainActivity.activityAor = aor
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
_uaMessages.value = uaMessages(aor)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("chats,$aor")
|
||||
returnResult()
|
||||
}
|
||||
|
||||
private fun returnResult() {
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun uaMessages(aor: String) : List<Message> {
|
||||
val res = mutableListOf<Message>()
|
||||
account.unreadMessages = false
|
||||
for (m in BaresipService.messages.reversed()) {
|
||||
if (m.aor != aor) continue
|
||||
var found = false
|
||||
for (r in res)
|
||||
if (r.peerUri == m.peerUri) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if (!found) {
|
||||
res.add(0, m)
|
||||
if (m.new)
|
||||
account.unreadMessages = true
|
||||
}
|
||||
}
|
||||
return res.toList()
|
||||
}
|
||||
}
|
||||
684
app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt
Normal file
684
app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt
Normal file
@ -0,0 +1,684 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.text.format.DateUtils.isToday
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.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.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
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 com.tutpro.baresip.BaresipService.Companion.contactNames
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.DropdownMenu
|
||||
import com.tutpro.baresip.CustomElements.LabelText
|
||||
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
fun NavGraphBuilder.chatsScreenRoute(navController: NavController) {
|
||||
composable(
|
||||
route = "chats/{aor}",
|
||||
arguments = listOf(navArgument("aor") { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
val aor = backStackEntry.arguments?.getString("aor")!!
|
||||
ChatsScreen(navController, aor)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ChatsScreen(navController: NavController, aor: String) {
|
||||
|
||||
val account = Account.ofAor(aor)!!
|
||||
val ctx = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val uaMessages: MutableState<List<Message>> = remember { mutableStateOf(emptyList<Message>()) }
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
Log.d(TAG, "Resumed to ChatsScreen for AOR: $aor")
|
||||
uaMessages.value = loadMessages(account)
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
var areMessagesLoaded by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(aor) {
|
||||
uaMessages.value = loadMessages(account)
|
||||
areMessagesLoaded = true
|
||||
}
|
||||
|
||||
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(navController, account, uaMessages)
|
||||
}
|
||||
},
|
||||
bottomBar = { NewChatPeer(ctx, navController, account) },
|
||||
content = { contentPadding ->
|
||||
if (areMessagesLoaded)
|
||||
ChatsContent(LocalContext.current, navController, contentPadding, account, uaMessages)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopAppBar(
|
||||
navController: NavController,
|
||||
account: Account,
|
||||
uaMessages: MutableState<List<Message>>
|
||||
) {
|
||||
|
||||
var menuExpanded by remember { mutableStateOf(false) }
|
||||
val delete = stringResource(R.string.delete)
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = String.format(
|
||||
stringResource(R.string.delete_chats_alert),
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
account.aor.substringAfter(":")
|
||||
),
|
||||
positiveButtonText = stringResource(R.string.delete),
|
||||
onPositiveClicked = positiveAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel),
|
||||
)
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.chats),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { menuExpanded = !menuExpanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
DropdownMenu (
|
||||
expanded = menuExpanded,
|
||||
onDismissRequest = { menuExpanded = false },
|
||||
items = listOf(delete),
|
||||
onItemClick = { selectedItem ->
|
||||
menuExpanded = false
|
||||
when (selectedItem) {
|
||||
delete -> {
|
||||
positiveAction.value = {
|
||||
Message.clearMessagesOfAor(account.aor)
|
||||
Message.save()
|
||||
uaMessages.value = listOf()
|
||||
account.unreadMessages = false
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatsContent(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
contentPadding: PaddingValues,
|
||||
account: Account,
|
||||
uaMessages: MutableState<List<Message>>
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Account(account)
|
||||
Chats(ctx, navController, account, uaMessages)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Account(account: Account) {
|
||||
val headerText = stringResource(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
account.aor.substringAfter(":")
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 8.dp),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chats(
|
||||
ctx: Context,
|
||||
navController: NavController,
|
||||
account: Account,
|
||||
uaMessages: MutableState<List<Message>>
|
||||
) {
|
||||
val aor = account.aor
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
val positiveButtonText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralButtonText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
if (showDialog.value)
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = positiveButtonText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralButtonText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp, end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
reverseLayout = true,
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items = uaMessages.value, key = { message -> message.timeStamp }) { message ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
when (val contact = Contact.findContact(message.peerUri)) {
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
CustomElements.ImageAvatar(avatarImage)
|
||||
else
|
||||
CustomElements.TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
CustomElements.TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
|
||||
null -> {
|
||||
val avatarImage = BitmapFactory
|
||||
.decodeResource(ctx.resources, R.drawable.person_image)
|
||||
CustomElements.ImageAvatar(avatarImage)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
val buttonShape = if (message.direction == MESSAGE_DOWN) {
|
||||
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
|
||||
} else {
|
||||
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp)
|
||||
}
|
||||
val borderStroke = if (account.unreadMessages && Message.unreadMessagesFromPeer(aor, message.peerUri)) {
|
||||
BorderStroke(width = 2.dp, color = LocalCustomColors.current.alert)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
CustomElements.Button(
|
||||
onClick = {
|
||||
navController.navigate("chat/${aor}/${message.peerUri}")
|
||||
},
|
||||
onLongClick = {
|
||||
val peer = Utils.friendlyUri(ctx, message.peerUri, account)
|
||||
val contactExists =
|
||||
Contact.nameExists(peer, BaresipService.contacts, false)
|
||||
if (contactExists) {
|
||||
dialogMessage.value = String.format(
|
||||
ctx.getString(R.string.short_chat_question),
|
||||
peer
|
||||
)
|
||||
positiveButtonText.value = ctx.getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
Message.deleteAorPeerMessages(aor, message.peerUri)
|
||||
uaMessages.value = loadMessages(account)
|
||||
}
|
||||
neutralButtonText.value = ""
|
||||
} else {
|
||||
dialogMessage.value =
|
||||
String.format(ctx.getString(R.string.long_chat_question), peer)
|
||||
positiveButtonText.value = ctx.getString(R.string.add_contact)
|
||||
positiveAction.value = {
|
||||
navController.navigate("baresip_contact/${message.peerUri}/new")
|
||||
}
|
||||
neutralButtonText.value = ctx.getString(R.string.delete)
|
||||
neutralAction.value = {
|
||||
Message.deleteAorPeerMessages(aor, message.peerUri)
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(end = 6.dp),
|
||||
shape = buttonShape,
|
||||
border = borderStroke,
|
||||
color =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryDark
|
||||
else
|
||||
LocalCustomColors.current.secondaryLight
|
||||
} else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryDark
|
||||
else
|
||||
LocalCustomColors.current.primaryLight
|
||||
},
|
||||
) {
|
||||
val peer = Utils.friendlyUri(ctx, message.peerUri, account)
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
val info = fmt.format(cal.time)
|
||||
Column {
|
||||
Row {
|
||||
val textColor =
|
||||
if (message.direction == MESSAGE_DOWN) {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.secondaryLight
|
||||
else
|
||||
LocalCustomColors.current.secondaryDark
|
||||
} else {
|
||||
if (BaresipService.darkTheme.value)
|
||||
LocalCustomColors.current.primaryLight
|
||||
else
|
||||
LocalCustomColors.current.primaryDark
|
||||
}
|
||||
Text(text = peer, color = textColor, fontSize = 12.sp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = info, color = textColor, fontSize = 12.sp)
|
||||
}
|
||||
Row {
|
||||
BasicText(
|
||||
text = message.message,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
|
||||
FontWeight.Bold else FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewChatPeer(ctx: Context, navController: NavController, account: Account) {
|
||||
|
||||
val alertTitle = remember { mutableStateOf("") }
|
||||
val alertMessage = remember { mutableStateOf("") }
|
||||
val showAlert = remember { mutableStateOf(false) }
|
||||
|
||||
fun makeChat(ctx: Context, navController: NavController, account: Account, chatPeer: String) {
|
||||
val peerUri = if (Utils.isTelNumber(chatPeer))
|
||||
"tel:$chatPeer"
|
||||
else
|
||||
chatPeer
|
||||
val uri = if (Utils.isTelUri(peerUri)) {
|
||||
if (account.telProvider == "") {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value =
|
||||
String.format(ctx.getString(R.string.no_telephony_provider), account.aor)
|
||||
showAlert.value = true
|
||||
""
|
||||
} else
|
||||
Utils.telToSip(peerUri, account)
|
||||
} else
|
||||
Utils.uriComplete(peerUri, account.aor)
|
||||
if (alertMessage.value.isEmpty()) {
|
||||
if (!Utils.checkUri(uri)) {
|
||||
alertTitle.value = ctx.getString(R.string.notice)
|
||||
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
|
||||
showAlert.value = true
|
||||
}
|
||||
else
|
||||
navController.navigate("chat/${account.aor}/${uri}")
|
||||
}
|
||||
}
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val items = remember { mutableStateOf(listOf<String>()) }
|
||||
val itemAction = remember { mutableStateOf<(Int) -> Unit>({ _ -> run {} }) }
|
||||
|
||||
SelectableAlertDialog(
|
||||
openDialog = showDialog,
|
||||
title = stringResource(R.string.choose_destination_uri),
|
||||
items = items.value,
|
||||
onItemClicked = itemAction.value,
|
||||
neutralButtonText = stringResource(R.string.cancel),
|
||||
onNeutralClicked = {}
|
||||
)
|
||||
|
||||
val suggestions by remember { contactNames }
|
||||
var filteredSuggestions by remember { mutableStateOf(suggestions) }
|
||||
var showSuggestions by remember { mutableStateOf(false) }
|
||||
val lazyListState = rememberLazyListState()
|
||||
val focusManager = LocalFocusManager.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var newPeer by remember { mutableStateOf("") }
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (showSuggestions && filteredSuggestions.isNotEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.shadow(8.dp, RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
LocalCustomColors.current.grayLight,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.animateContentSize()
|
||||
) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 150.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 180.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
state = lazyListState
|
||||
) {
|
||||
items(
|
||||
items = filteredSuggestions,
|
||||
key = { suggestion -> suggestion }
|
||||
) { suggestion ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
newPeer = suggestion
|
||||
showSuggestions = false
|
||||
}
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = suggestion,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = LocalCustomColors.current.grayDark,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = newPeer,
|
||||
placeholder = { Text(stringResource(R.string.new_chat_peer)) },
|
||||
onValueChange = {
|
||||
newPeer = it
|
||||
showSuggestions = newPeer.length > 2
|
||||
filteredSuggestions = if (it.isEmpty()) {
|
||||
suggestions
|
||||
} else {
|
||||
suggestions.filter { suggestion ->
|
||||
newPeer.length > 2 && suggestion.startsWith(
|
||||
newPeer,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 6.dp)
|
||||
.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (newPeer.isNotEmpty())
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
if (showSuggestions)
|
||||
showSuggestions = false
|
||||
else
|
||||
newPeer = ""
|
||||
}
|
||||
)
|
||||
},
|
||||
label = {
|
||||
LabelText(stringResource(R.string.new_chat_peer))
|
||||
},
|
||||
textStyle = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = LocalCustomColors.current.itemText
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Done
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
SmallFloatingActionButton(
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
onClick = {
|
||||
showSuggestions = false
|
||||
val peerText = newPeer.trim()
|
||||
if (peerText.isNotEmpty()) {
|
||||
val uris = Contact.contactUris(peerText)
|
||||
if (uris.isEmpty())
|
||||
makeChat(ctx, navController, account, peerText)
|
||||
else if (uris.size == 1)
|
||||
makeChat(ctx, navController, account, uris[0])
|
||||
else {
|
||||
items.value = uris
|
||||
itemAction.value = { index ->
|
||||
makeChat(ctx, navController, account, uris[index])
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
}
|
||||
newPeer = ""
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
containerColor = LocalCustomColors.current.accent,
|
||||
contentColor = LocalCustomColors.current.background
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
modifier = Modifier.size(36.dp),
|
||||
contentDescription = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMessages(account: Account) : List<Message> {
|
||||
val res = mutableListOf<Message>()
|
||||
account.unreadMessages = false
|
||||
for (m in BaresipService.messages.reversed()) {
|
||||
if (m.aor != account.aor) continue
|
||||
var found = false
|
||||
for (r in res)
|
||||
if (r.peerUri == m.peerUri) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if (!found) {
|
||||
res.add(0, m)
|
||||
if (m.new)
|
||||
account.unreadMessages = true
|
||||
}
|
||||
}
|
||||
return res.toList()
|
||||
}
|
||||
@ -1,384 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.safeDrawingPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
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 com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
class CodecsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var acc: Account
|
||||
private lateinit var ua: UserAgent
|
||||
|
||||
private lateinit var allCodecs: List<String>
|
||||
private lateinit var accCodecs: List<String>
|
||||
private lateinit var codecs: SnapshotStateList<Codec>
|
||||
|
||||
private var aor = ""
|
||||
private var media = ""
|
||||
private var title = ""
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
media = intent.getStringExtra("media")!!
|
||||
|
||||
Utils.addActivity("codecs,$aor,$media")
|
||||
|
||||
ua = UserAgent.ofAor(aor)!!
|
||||
acc = ua.account
|
||||
|
||||
if (media == "audio") {
|
||||
title = getString(R.string.audio_codecs)
|
||||
allCodecs = ArrayList(Api.audio_codecs().split(","))
|
||||
accCodecs = acc.audioCodec
|
||||
} else {
|
||||
title = getString(R.string.video_codecs)
|
||||
allCodecs = ArrayList(Api.video_codecs().split(",").distinct())
|
||||
accCodecs = acc.videoCodec
|
||||
}
|
||||
|
||||
val currentCodecs = mutableListOf<Codec>()
|
||||
for (codec in accCodecs)
|
||||
currentCodecs.add(Codec(codec, mutableStateOf(true)))
|
||||
for (codec in allCodecs)
|
||||
if (codec !in accCodecs)
|
||||
currentCodecs.add(Codec(codec, mutableStateOf(false)))
|
||||
|
||||
codecs = mutableStateListOf<Codec>().apply { addAll(currentCodecs) }
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
CodecsScreen { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CodecsScreen(navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.codecs),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
updateCodecs()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
CodecsContent(contentPadding)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CodecsContent(contentPadding: PaddingValues) {
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(bottom = 16.dp),
|
||||
) {
|
||||
Title()
|
||||
Codecs(codecs = codecs, onCodecsChange = { updatedCodecs ->
|
||||
onCodecsChange(updatedCodecs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCodecsChange(updatedCodecs: List<Codec>) {
|
||||
codecs.clear()
|
||||
codecs.addAll(updatedCodecs)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Title() {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.clickable {
|
||||
if (media == "audio") {
|
||||
alertTitle.value = getString(R.string.audio_codecs)
|
||||
alertMessage.value = getString(R.string.audio_codecs_help)
|
||||
}
|
||||
else {
|
||||
alertTitle.value = getString(R.string.video_codecs)
|
||||
alertMessage.value = getString(R.string.video_codecs_help)
|
||||
}
|
||||
showAlert.value = true
|
||||
},
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Codecs(codecs: SnapshotStateList<Codec>, onCodecsChange: (List<Codec>) -> Unit) {
|
||||
|
||||
val draggableState = rememberDraggableListState(
|
||||
onMove = { fromIndex, toIndex ->
|
||||
codecs.add(toIndex, codecs.removeAt(fromIndex))
|
||||
onCodecsChange(codecs.toList())
|
||||
}
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = draggableState.listState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
state = draggableState.listState,
|
||||
contentPadding = PaddingValues(start = 12.dp, end = 12.dp),
|
||||
//verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
draggableItems(
|
||||
state = draggableState,
|
||||
items = codecs,
|
||||
key = { item -> item.name }
|
||||
) { item, isDragging ->
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = if (isDragging)
|
||||
LocalCustomColors.current.grayLight
|
||||
else
|
||||
LocalCustomColors.current.background
|
||||
),
|
||||
headlineContent = {
|
||||
Text(text = item.name,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.alpha(if (item.enabled.value) 1.0f else 0.5f)
|
||||
.padding(start = 6.dp)
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
item.enabled.value = !item.enabled.value
|
||||
if (item.enabled.value) {
|
||||
val index = codecs.indexOf(item)
|
||||
codecs.removeAt(index)
|
||||
codecs.add(0, item)
|
||||
}
|
||||
else {
|
||||
val index = codecs.indexOf(item)
|
||||
codecs.removeAt(index)
|
||||
codecs.add(item)
|
||||
}
|
||||
onCodecsChange(codecs.toList())
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
modifier = Modifier.dragHandle(
|
||||
state = draggableState,
|
||||
key = item.name
|
||||
),
|
||||
imageVector =ImageVector.vectorResource(R.drawable.reorder),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
)
|
||||
if (codecs.indexOf(item) > 0)
|
||||
HorizontalDivider(
|
||||
color = LocalCustomColors.current.gray,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
thickness = 1.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCodecs() {
|
||||
|
||||
var save = false
|
||||
val newCodecs = ArrayList<String>()
|
||||
|
||||
for (codec in codecs)
|
||||
if (codec.enabled.value)
|
||||
newCodecs.add(codec.name)
|
||||
|
||||
val codecList = Utils.implode(newCodecs, ",")
|
||||
|
||||
if (media == "audio")
|
||||
if (newCodecs != acc.audioCodec) {
|
||||
if (Api.account_set_audio_codecs(acc.accp, codecList) == 0) {
|
||||
Log.d(TAG, "New audio codecs '$codecList'")
|
||||
acc.audioCodec = newCodecs
|
||||
save = true
|
||||
} else {
|
||||
Log.e(TAG, "Setting of audio codecs '$codecList' failed")
|
||||
}
|
||||
}
|
||||
|
||||
if (media == "video")
|
||||
if (newCodecs != acc.videoCodec) {
|
||||
if (Api.account_set_video_codecs(acc.accp, codecList) == 0) {
|
||||
Log.d(TAG, "New video codecs '$codecs'")
|
||||
acc.videoCodec = newCodecs
|
||||
save = true
|
||||
} else {
|
||||
Log.e(TAG, "Setting of video codecs '$codecs' failed")
|
||||
}
|
||||
}
|
||||
|
||||
if (save)
|
||||
AccountsActivity.saveAccounts()
|
||||
|
||||
BaresipService.activities.remove("codecs,$aor,$media")
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("codecs,$aor,$media")
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
MainActivity.activityAor = aor
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
301
app/src/main/kotlin/com/tutpro/baresip/CodecsScreen.kt
Normal file
301
app/src/main/kotlin/com/tutpro/baresip/CodecsScreen.kt
Normal file
@ -0,0 +1,301 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
fun NavGraphBuilder.codecsScreenRoute(navController: NavController) {
|
||||
composable(
|
||||
route = "codecs/{aor}/{media}",
|
||||
arguments = listOf(
|
||||
navArgument("aor") { type = NavType.StringType },
|
||||
navArgument("media") { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
val aor = backStackEntry.arguments?.getString("aor")!!
|
||||
val media = backStackEntry.arguments?.getString("media")!!
|
||||
val account = UserAgent.ofAor(aor)?.account!!
|
||||
CodecsScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
checkOnClick = { updatedCodecs ->
|
||||
val enabledCodecNames = updatedCodecs.filter { it.enabled.value }.map { it.name }
|
||||
val codecList = Utils.implode(enabledCodecNames, ",")
|
||||
Log.d(TAG, "Saving codecs for ${account.aor} (${media}): $codecList")
|
||||
val success = if (media == "audio")
|
||||
Api.account_set_audio_codecs(account.accp, codecList)
|
||||
else
|
||||
Api.account_set_video_codecs(account.accp, codecList)
|
||||
if (success == 0) {
|
||||
if (media == "audio")
|
||||
account.audioCodec = enabledCodecNames as ArrayList<String>
|
||||
else
|
||||
account.videoCodec = enabledCodecNames as ArrayList<String>
|
||||
Account.saveAccounts()
|
||||
Log.d("CodecsSave", "Codecs saved successfully.")
|
||||
}
|
||||
else
|
||||
Log.e(TAG, "Failed to set $aor codecs.")
|
||||
navController.popBackStack()
|
||||
},
|
||||
aor = aor,
|
||||
media = media
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CodecsScreen(
|
||||
onBack: () -> Unit,
|
||||
checkOnClick: (List<Codec>) -> Unit,
|
||||
aor: String,
|
||||
media: String
|
||||
) {
|
||||
val ua = UserAgent.ofAor(aor)!!
|
||||
val acc = ua.account
|
||||
var currentCodecsState by remember { mutableStateOf<List<Codec>>(emptyList()) }
|
||||
|
||||
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 = if (media == "audio")
|
||||
stringResource(R.string.audio_codecs)
|
||||
else
|
||||
stringResource(R.string.video_codecs),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
checkOnClick(currentCodecsState)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
CodecsContent(
|
||||
contentPadding,
|
||||
acc,
|
||||
media,
|
||||
onUpdateCodecs = { updatedCodecs -> currentCodecsState = updatedCodecs }
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private val alertTitle = mutableStateOf("")
|
||||
private val alertMessage = mutableStateOf("")
|
||||
private val showAlert = mutableStateOf(false)
|
||||
|
||||
|
||||
@Composable
|
||||
private fun CodecsContent(
|
||||
contentPadding: PaddingValues,
|
||||
acc: Account,
|
||||
media: String,
|
||||
onUpdateCodecs: (List<Codec>) -> Unit
|
||||
) {
|
||||
val codecs = remember { mutableStateListOf<Codec>() }
|
||||
|
||||
LaunchedEffect(acc, media) {
|
||||
val allCodecs: List<String> = if (media == "audio") {
|
||||
Api.audio_codecs().split(",")
|
||||
} else {
|
||||
Api.video_codecs().split(",").distinct()
|
||||
}
|
||||
val accCodecs: List<String> = if (media == "audio") {
|
||||
acc.audioCodec
|
||||
} else {
|
||||
acc.videoCodec
|
||||
}
|
||||
val currentCodecs = mutableListOf<Codec>()
|
||||
for (codec in accCodecs)
|
||||
currentCodecs.add(Codec(codec, mutableStateOf(true)))
|
||||
for (codec in allCodecs)
|
||||
if (codec !in accCodecs)
|
||||
currentCodecs.add(Codec(codec, mutableStateOf(false)))
|
||||
codecs.clear()
|
||||
codecs.addAll(currentCodecs)
|
||||
}
|
||||
|
||||
if (showAlert.value) {
|
||||
AlertDialog(
|
||||
showDialog = showAlert,
|
||||
title = alertTitle.value,
|
||||
message = alertMessage.value,
|
||||
positiveButtonText = stringResource(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(bottom = 16.dp),
|
||||
) {
|
||||
Codecs(
|
||||
codecs = codecs,
|
||||
onUpdateCodecs = onUpdateCodecs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun Codecs(codecs: SnapshotStateList<Codec>, onUpdateCodecs: (List<Codec>) -> Unit) {
|
||||
|
||||
val draggableState = rememberDraggableListState(
|
||||
onMove = { fromIndex, toIndex ->
|
||||
codecs.add(toIndex, codecs.removeAt(fromIndex))
|
||||
onUpdateCodecs(codecs.toList())
|
||||
}
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = draggableState.listState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
state = draggableState.listState,
|
||||
contentPadding = PaddingValues(start = 12.dp, end = 12.dp),
|
||||
//verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
draggableItems(
|
||||
state = draggableState,
|
||||
items = codecs,
|
||||
key = { item -> item.name }
|
||||
) { item, isDragging ->
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = if (isDragging)
|
||||
LocalCustomColors.current.grayLight
|
||||
else
|
||||
LocalCustomColors.current.background
|
||||
),
|
||||
headlineContent = {
|
||||
Text(text = item.name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (item.enabled.value) 1.0f else 0.5f)
|
||||
.padding(start = 6.dp)
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
item.enabled.value = !item.enabled.value
|
||||
if (item.enabled.value) {
|
||||
val index = codecs.indexOf(item)
|
||||
codecs.removeAt(index)
|
||||
codecs.add(0, item)
|
||||
} else {
|
||||
val index = codecs.indexOf(item)
|
||||
codecs.removeAt(index)
|
||||
codecs.add(item)
|
||||
}
|
||||
onUpdateCodecs(codecs.toList())
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
modifier = Modifier.dragHandle(
|
||||
state = draggableState,
|
||||
key = item.name
|
||||
),
|
||||
imageVector =ImageVector.vectorResource(R.drawable.reorder),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
)
|
||||
if (codecs.indexOf(item) > 0)
|
||||
HorizontalDivider(
|
||||
color = LocalCustomColors.current.gray,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
thickness = 1.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets
|
||||
object Config {
|
||||
|
||||
private val configPath = BaresipService.filesPath + "/config"
|
||||
val audioModules = listOf("opus", "amr", "g722", "g7221", "g726", "g729", "codec2", "g711")
|
||||
private lateinit var config: String
|
||||
private lateinit var previousConfig: String
|
||||
private lateinit var previousLines: List<String>
|
||||
@ -18,7 +19,7 @@ object Config {
|
||||
|
||||
config = ctx.assets.open("config.static").bufferedReader().use { it.readText() }
|
||||
if (!File(configPath).exists()) {
|
||||
for (module in AudioActivity.audioModules)
|
||||
for (module in audioModules)
|
||||
config = "${config}module ${module}.so\n"
|
||||
previousConfig = config
|
||||
} else {
|
||||
@ -149,7 +150,7 @@ object Config {
|
||||
}
|
||||
|
||||
val previousModules = previousVariables("module")
|
||||
for (module in AudioActivity.audioModules)
|
||||
for (module in audioModules)
|
||||
if ("${module}.so" in previousModules)
|
||||
config = "${config}module ${module}.so\n"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@ const val MESSAGE_REQ_CODE = 8
|
||||
const val REPLY_REQ_CODE = 9
|
||||
const val SAVE_REQ_CODE = 10
|
||||
const val DELETE_REQ_CODE = 11
|
||||
const val DIRECT_REPLY_REQ_CODE = 12
|
||||
|
||||
const val REGISTRATION_INTERVAL = 900
|
||||
const val NO_AUTH_PASS = "t%Qa?~?J8,~6"
|
||||
@ -32,6 +33,10 @@ const val MESSAGE_UP = 2131165306
|
||||
const val MESSAGE_UP_FAIL = 2131165307
|
||||
const val MESSAGE_UP_WAIT = 2131165308
|
||||
|
||||
val mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF", "srtp-mand" to "SRTP-MAND",
|
||||
"srtp" to "SRTP", "" to "--")
|
||||
|
||||
val mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,458 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.provider.ContactsContract
|
||||
import android.window.OnBackInvokedCallback
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
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.draw.clip
|
||||
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.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Observer
|
||||
import coil.compose.AsyncImage
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class ContactsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var aor: String
|
||||
private var newAndroidName: String? = null
|
||||
private var lastClick: Long = 0
|
||||
|
||||
private var backInvokedCallback: OnBackInvokedCallback? = null
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
@RequiresApi(33)
|
||||
private fun registerBackInvokedCallback() {
|
||||
backInvokedCallback = OnBackInvokedCallback { goBack() }
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
||||
backInvokedCallback!!
|
||||
)
|
||||
}
|
||||
|
||||
private val contactRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
if (it.data != null && it.data!!.hasExtra("name"))
|
||||
newAndroidName = it.data!!.getStringExtra("name")
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33)
|
||||
registerBackInvokedCallback()
|
||||
else {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
val title = getString(R.string.contacts)
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
Utils.addActivity("contacts,$aor")
|
||||
|
||||
val androidContactsObserver = Observer<Long> {
|
||||
if (newAndroidName != null) {
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(ContactsContract.Contacts.STARRED, 1)
|
||||
try {
|
||||
this.contentResolver.update(
|
||||
ContactsContract.RawContacts.CONTENT_URI, contentValues,
|
||||
ContactsContract.Contacts.DISPLAY_NAME + "='" + newAndroidName + "'", null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Update of Android favorite failed: ${e.message}")
|
||||
}
|
||||
newAndroidName = null
|
||||
}
|
||||
//Contact.contactsUpdate()
|
||||
}
|
||||
BaresipService.contactUpdate.observe(this, androidContactsObserver)
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ContactsScreen(LocalContext.current, title) {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContactsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
SmallFloatingActionButton(onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", "")
|
||||
intent.putExtras(b)
|
||||
contactRequest.launch(intent)
|
||||
}},
|
||||
containerColor = LocalCustomColors.current.accent,
|
||||
contentColor = LocalCustomColors.current.background
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Add,
|
||||
modifier = Modifier.size(36.dp),
|
||||
contentDescription = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
ContactsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ContactsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
val positiveText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
if (showDialog.value)
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = positiveText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 64.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(BaresipService.contacts, key = { it.id() }) { contact ->
|
||||
|
||||
val name = contact.name()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
when(contact) {
|
||||
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
Image(
|
||||
bitmap = avatarImage.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
else
|
||||
TextAvatar(name, contact.color)
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
TextAvatar(name, contact.color)
|
||||
}
|
||||
}
|
||||
|
||||
when(contact) {
|
||||
|
||||
is Contact.BaresipContact -> {
|
||||
Text(text = name,
|
||||
fontSize = 20.sp,
|
||||
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
dialogMessage.value = String.format(
|
||||
getString(R.string.contact_action_question),
|
||||
name
|
||||
)
|
||||
positiveText.value = getString(R.string.call)
|
||||
positiveAction.value = {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "call")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null)
|
||||
Log.w(TAG, "onClickListener did not find AoR $aor")
|
||||
else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", contact.uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
neutralText.value = getString(R.string.send_message)
|
||||
neutralAction.value = {
|
||||
val i = Intent(
|
||||
ctx,
|
||||
MainActivity::class.java
|
||||
)
|
||||
i.flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "message")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null)
|
||||
Log.w(TAG, "onClickListener did not find AoR $aor")
|
||||
else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", contact.uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
onLongClick = {
|
||||
dialogMessage.value = String.format(
|
||||
getString(R.string.contact_delete_question),
|
||||
name
|
||||
)
|
||||
positiveText.value = getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
val id = contact.id
|
||||
val avatarFile = File(
|
||||
BaresipService.filesPath,
|
||||
"$id.png"
|
||||
)
|
||||
if (avatarFile.exists()) {
|
||||
try {
|
||||
avatarFile.delete()
|
||||
} catch (e: IOException) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Could not delete file $id.png: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
Contact.removeBaresipContact(contact)
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
)
|
||||
)
|
||||
SmallFloatingActionButton(
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", false)
|
||||
b.putString("name", name)
|
||||
intent.putExtras(b)
|
||||
contactRequest.launch(intent)
|
||||
}
|
||||
},
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
contentColor = LocalCustomColors.current.secondary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Edit,
|
||||
modifier = Modifier.size(28.dp),
|
||||
contentDescription = stringResource(R.string.edit)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
Text(text = name,
|
||||
fontSize = 20.sp,
|
||||
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp, top = 4.dp, bottom = 4.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val i =
|
||||
Intent(ctx, AndroidContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("name", name)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i, null)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
dialogMessage.value = String.format(
|
||||
getString(R.string.contact_delete_question),
|
||||
name
|
||||
)
|
||||
positiveText.value = getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
ctx.contentResolver.delete(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
|
||||
null
|
||||
)
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (backInvokedCallback != null)
|
||||
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
||||
}
|
||||
else
|
||||
onBackPressedCallback.remove()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("contacts,$aor")
|
||||
setResult(RESULT_OK, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
332
app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt
Normal file
332
app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt
Normal file
@ -0,0 +1,332 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.ContactsContract
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
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.draw.clip
|
||||
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.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import coil.compose.AsyncImage
|
||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
const val avatarSize: Int = 96
|
||||
|
||||
fun NavGraphBuilder.contactsScreenRoute(
|
||||
navController: NavController,
|
||||
viewModel: ViewModel
|
||||
) {
|
||||
composable("contacts") { backStackEntry ->
|
||||
ContactsScreen(navController = navController, viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContactsScreen(navController: NavController, viewModel: ViewModel) {
|
||||
|
||||
val ctx = LocalContext.current
|
||||
|
||||
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.contacts),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
SmallFloatingActionButton(onClick = { navController.navigate("baresip_contact//new") },
|
||||
containerColor = LocalCustomColors.current.accent,
|
||||
contentColor = LocalCustomColors.current.background
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Add,
|
||||
modifier = Modifier.size(36.dp),
|
||||
contentDescription = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
ContactsContent(ctx, viewModel, navController, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ContactsContent(
|
||||
ctx: Context,
|
||||
viewModel: ViewModel,
|
||||
navController: NavController,
|
||||
contentPadding: PaddingValues
|
||||
) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogMessage = remember { mutableStateOf("") }
|
||||
val positiveText = remember { mutableStateOf("") }
|
||||
val positiveAction = remember { mutableStateOf({}) }
|
||||
val neutralText = remember { mutableStateOf("") }
|
||||
val neutralAction = remember { mutableStateOf({}) }
|
||||
|
||||
if (showDialog.value)
|
||||
AlertDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.confirmation),
|
||||
message = dialogMessage.value,
|
||||
positiveButtonText = positiveText.value,
|
||||
onPositiveClicked = positiveAction.value,
|
||||
neutralButtonText = neutralText.value,
|
||||
onNeutralClicked = neutralAction.value,
|
||||
negativeButtonText = stringResource(R.string.cancel)
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 64.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(BaresipService.contacts, key = { it.id() }) { contact ->
|
||||
|
||||
val name = contact.name()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
when(contact) {
|
||||
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
Image(
|
||||
bitmap = avatarImage.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
else
|
||||
TextAvatar(name, contact.color)
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
TextAvatar(name, contact.color)
|
||||
}
|
||||
}
|
||||
|
||||
when(contact) {
|
||||
|
||||
is Contact.BaresipContact -> {
|
||||
Text(text = name,
|
||||
fontSize = 20.sp,
|
||||
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
val aor = viewModel.selectedAor.value
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
if (ua != null) {
|
||||
intent.putExtra("uap", ua.uap)
|
||||
intent.putExtra("peer", contact.uri)
|
||||
}
|
||||
else
|
||||
Log.w(TAG, "onClickListener did not find UA for $aor")
|
||||
dialogMessage.value = String.format(
|
||||
ctx.getString(R.string.contact_action_question),
|
||||
name
|
||||
)
|
||||
positiveText.value = ctx.getString(R.string.call)
|
||||
positiveAction.value = {
|
||||
if (ua != null) {
|
||||
handleIntent(ctx, viewModel, intent, "call")
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
neutralText.value = ctx.getString(R.string.send_message)
|
||||
neutralAction.value = {
|
||||
if (ua != null) {
|
||||
handleIntent(ctx, viewModel, intent, "message")
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
},
|
||||
onLongClick = {
|
||||
dialogMessage.value = String.format(
|
||||
ctx.getString(R.string.contact_delete_question),
|
||||
name
|
||||
)
|
||||
positiveText.value = ctx.getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
val id = contact.id
|
||||
val avatarFile = File(
|
||||
BaresipService.filesPath,
|
||||
"$id.png"
|
||||
)
|
||||
if (avatarFile.exists()) {
|
||||
try {
|
||||
avatarFile.delete()
|
||||
} catch (e: IOException) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Could not delete file $id.png: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
Contact.removeBaresipContact(contact)
|
||||
}
|
||||
showDialog.value = true
|
||||
}
|
||||
)
|
||||
)
|
||||
SmallFloatingActionButton(
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
onClick = { navController.navigate("baresip_contact/${name}/old") },
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
contentColor = LocalCustomColors.current.secondary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Edit,
|
||||
modifier = Modifier.size(28.dp),
|
||||
contentDescription = stringResource(R.string.edit)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
Text(text = name,
|
||||
fontSize = 20.sp,
|
||||
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp, top = 4.dp, bottom = 4.dp)
|
||||
.combinedClickable(
|
||||
onClick = { navController.navigate("android_contact/${name}") },
|
||||
onLongClick = {
|
||||
dialogMessage.value = String.format(
|
||||
ctx.getString(R.string.contact_delete_question),
|
||||
name
|
||||
)
|
||||
positiveText.value = ctx.getString(R.string.delete)
|
||||
positiveAction.value = {
|
||||
ctx.contentResolver.delete(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
|
||||
null
|
||||
)
|
||||
}
|
||||
neutralText.value = ""
|
||||
showDialog.value = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import android.graphics.Bitmap
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.ScrollState
|
||||
@ -117,12 +118,14 @@ object CustomElements {
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape,
|
||||
border: BorderStroke? = null,
|
||||
color: Color,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Surface(
|
||||
shape = shape,
|
||||
color = color,
|
||||
border = border,
|
||||
modifier = modifier
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
@ -491,7 +494,7 @@ object CustomElements {
|
||||
BasicAlertDialog(
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = false,
|
||||
dismissOnClickOutside = false
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
onDismissRequest = {
|
||||
keyboardController?.hide()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2617
app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt
Normal file
2617
app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -52,7 +52,7 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
|
||||
|
||||
fun deleteAorMessage(aor: String, time: Long) {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
for (message in updatedMessages)
|
||||
for (message in updatedMessages.reversed())
|
||||
if (message.aor == aor && message.timeStamp == time) {
|
||||
updatedMessages.remove(message)
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
@ -72,7 +72,7 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
|
||||
|
||||
fun updateAorMessage(aor: String, time: Long) {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
for (message in updatedMessages)
|
||||
for (message in updatedMessages.reversed())
|
||||
if (message.aor == aor && message.timeStamp == time) {
|
||||
message.new = false
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
@ -81,6 +81,35 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
|
||||
}
|
||||
}
|
||||
|
||||
fun unreadMessages(aor: String): Boolean {
|
||||
for (message in BaresipService.messages.reversed())
|
||||
if (message.aor == aor && message.new)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun unreadMessagesFromPeer(aor: String, peerUri: String): Boolean {
|
||||
for (message in BaresipService.messages.reversed())
|
||||
if (message.aor == aor && message.peerUri == peerUri && message.new)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun updateMessagesFromPearRead(aor: String, peerUri: String): Boolean {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
var updated = false
|
||||
for (message in updatedMessages)
|
||||
if (message.aor == aor && message.peerUri == peerUri && message.new) {
|
||||
message.new = false
|
||||
updated = true
|
||||
}
|
||||
if (updated) {
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
save()
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val file = File(BaresipService.filesPath, "messages")
|
||||
try {
|
||||
|
||||
1428
app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt
Normal file
1428
app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -31,13 +31,13 @@ class TaskReceiver : BroadcastReceiver() {
|
||||
Api.account_set_regint(acc.accp, REGISTRATION_INTERVAL)
|
||||
Api.ua_register(ua.uap)
|
||||
acc.regint = Api.account_regint(acc.accp)
|
||||
AccountsActivity.saveAccounts()
|
||||
Account.saveAccounts()
|
||||
} else {
|
||||
Log.d(TAG, "TaskReceiver: un-registering $aor")
|
||||
Api.account_set_regint(acc.accp, 0)
|
||||
Api.ua_unregister(ua.uap)
|
||||
acc.regint = Api.account_regint(acc.accp)
|
||||
AccountsActivity.saveAccounts()
|
||||
Account.saveAccounts()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +45,23 @@ class UserAgent(val uap: Long) {
|
||||
return null
|
||||
}
|
||||
|
||||
fun reRegister() {
|
||||
this.status = R.drawable.circle_yellow
|
||||
if (this.account.regint == 0)
|
||||
Api.ua_unregister(this.uap)
|
||||
else
|
||||
Api.ua_register(this.uap)
|
||||
}
|
||||
|
||||
fun makeDefault() {
|
||||
val index = uas.value.indexOf(this)
|
||||
val updatedUas = uas.value.toMutableList()
|
||||
updatedUas.removeAt(index)
|
||||
updatedUas.add(0, this)
|
||||
uas.value = updatedUas.toList()
|
||||
uasStatus.value = statusMap()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun ofAor(aor: String): UserAgent? {
|
||||
|
||||
@ -7,7 +7,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
@ -62,7 +61,6 @@ import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import androidx.core.graphics.scale
|
||||
|
||||
object Utils {
|
||||
|
||||
@ -231,7 +229,7 @@ object Utils {
|
||||
fun checkUriUser(user: String): Boolean {
|
||||
val escaped = """%(\d|A|B|C|D|E|F|a|b|c|d|e|f){2}""".toRegex()
|
||||
escaped.replace(user, "").forEach {
|
||||
if (!(it.isLetterOrDigit() || "-_.!~*\'()&=+\$,;?/".contains(it))) return false }
|
||||
if (!(it.isLetterOrDigit() || "-_.!~*\'()&=+$,;?/".contains(it))) return false }
|
||||
return user.isNotEmpty() && !checkIpV4(user) && !checkIpV6(user)
|
||||
}
|
||||
|
||||
@ -239,7 +237,7 @@ object Utils {
|
||||
val parts = domain.split(".")
|
||||
for (p in parts) {
|
||||
if (p.endsWith("-") || p.startsWith("-") ||
|
||||
!Regex("^[-a-zA-Z0-9]+\$").matches(p))
|
||||
!Regex("^[-a-zA-Z0-9]+$").matches(p))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -333,7 +331,7 @@ object Utils {
|
||||
}
|
||||
|
||||
fun isTelNumber(no: String): Boolean {
|
||||
return no.isNotEmpty() && Regex("^([+][1-9])?[0-9- (),*#]{0,24}\$").matches(no)
|
||||
return no.isNotEmpty() && Regex("^([+][1-9])?[0-9- (),*#]{0,24}$").matches(no)
|
||||
}
|
||||
|
||||
fun isTelUri(uri: String): Boolean {
|
||||
@ -383,7 +381,7 @@ object Utils {
|
||||
}
|
||||
|
||||
private fun checkToken(token: String): Boolean {
|
||||
return Regex("^[-a-zA-Z0-9.!%*_+`'~]+\$").matches(token)
|
||||
return Regex("^[-a-zA-Z0-9.!%*_+`'~]+$").matches(token)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@ -585,22 +583,6 @@ object Utils {
|
||||
name
|
||||
}
|
||||
|
||||
fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
|
||||
if (file.exists()) file.delete()
|
||||
try {
|
||||
val out = FileOutputStream(file)
|
||||
val scaledBitmap = bitmap.scale(96, 96)
|
||||
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
Log.d(TAG, "Saved bitmap to ${file.absolutePath} of length ${file.length()}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save bitmap to ${file.absolutePath}: $e")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID: Long = -29238082928391L
|
||||
@ -805,11 +787,6 @@ object Utils {
|
||||
rnd.nextInt(256))
|
||||
}
|
||||
|
||||
fun addActivity(activity: String) {
|
||||
if ((BaresipService.activities.isEmpty()) || (BaresipService.activities[0] != activity))
|
||||
BaresipService.activities.add(0, activity)
|
||||
}
|
||||
|
||||
fun requestDismissKeyguard(activity: Activity) {
|
||||
val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
kgm.requestDismissKeyguard(activity, null)
|
||||
@ -1018,4 +995,39 @@ object Utils {
|
||||
Api.AAudio_close_stream()
|
||||
|
||||
}
|
||||
|
||||
/*fun listFilesInDirectory(directoryPath: String): List<File> {
|
||||
val directory = File(directoryPath)
|
||||
if (!directory.exists()) {
|
||||
Log.w(TAG, "Directory does not exist: $directoryPath")
|
||||
return emptyList()
|
||||
}
|
||||
if (!directory.isDirectory) {
|
||||
Log.w(TAG, "Path is not a directory: $directoryPath")
|
||||
return emptyList()
|
||||
}
|
||||
val files = directory.listFiles()
|
||||
if (files == null) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Failed to list files in directory (listFiles returned null): $directoryPath"
|
||||
)
|
||||
return emptyList()
|
||||
}
|
||||
return files.filter { it.isFile }
|
||||
}*/
|
||||
|
||||
/*@SuppressLint("RestrictedApi")
|
||||
fun printBackStack(navController: NavController) {
|
||||
Log.e(TAG, "---- Current Navigation Back Stack ----")
|
||||
navController.currentBackStack.value.forEachIndexed { index, navBackStackEntry ->
|
||||
val route = navBackStackEntry.destination.route
|
||||
val arguments = navBackStackEntry.arguments?.let { bundle ->
|
||||
bundle.keySet().joinToString(", ") { key -> "$key=${bundle.get(key)}" }
|
||||
} ?: "null"
|
||||
Log.e(TAG, "$index: Route='${route}', Args=[$arguments], ID=${navBackStackEntry.id}")
|
||||
}
|
||||
Log.e(TAG, "--------------------------------------")
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
@ -1,10 +1,23 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class NavigationCommand {
|
||||
data class NavigateToChat(val aor: String, val peer: String?) : NavigationCommand()
|
||||
data class NavigateToCalls(val aor: String) : NavigationCommand()
|
||||
object NavigateToHome: NavigationCommand()
|
||||
// Add other navigation commands as needed
|
||||
}
|
||||
|
||||
class ViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
@ -15,4 +28,96 @@ class ViewModel(application: Application) : AndroidViewModel(application) {
|
||||
_selectedAor.value = newValue
|
||||
}
|
||||
|
||||
private val _speakerIcon = MutableStateFlow(R.drawable.speaker_off)
|
||||
val speakerIcon: StateFlow<Int> = _speakerIcon.asStateFlow()
|
||||
|
||||
fun updateSpeakerIcon(iconResId: Int) {
|
||||
_speakerIcon.value = iconResId
|
||||
}
|
||||
|
||||
private val _micIcon = MutableStateFlow(R.drawable.mic_on)
|
||||
val micIcon: StateFlow<Int> = _micIcon.asStateFlow()
|
||||
|
||||
fun updateMicIcon(iconResId: Int) {
|
||||
_micIcon.value = iconResId
|
||||
}
|
||||
|
||||
private val _vmIcon = MutableStateFlow(R.drawable.voicemail)
|
||||
val vmIcon: StateFlow<Int> = _vmIcon.asStateFlow()
|
||||
|
||||
fun updateVmIcon(newIcon: Int) {
|
||||
_vmIcon.value = newIcon
|
||||
}
|
||||
|
||||
private val _showVmIcon = MutableStateFlow(false)
|
||||
val showVmIcon: StateFlow<Boolean> = _showVmIcon.asStateFlow()
|
||||
|
||||
fun updateShowVmIcon(show: Boolean) {
|
||||
_showVmIcon.value = show
|
||||
}
|
||||
|
||||
private val _messagesIcon = MutableStateFlow(R.drawable.messages)
|
||||
val messagesIcon: StateFlow<Int> = _messagesIcon.asStateFlow()
|
||||
|
||||
fun updateMessagesIcon(newIcon: Int) {
|
||||
_messagesIcon.value = newIcon
|
||||
}
|
||||
|
||||
private val _callsIcon = MutableStateFlow(R.drawable.calls)
|
||||
val callsIcon: StateFlow<Int> = _callsIcon.asStateFlow()
|
||||
|
||||
fun updateCallsIcon(newIcon: Int) {
|
||||
_callsIcon.value = newIcon
|
||||
}
|
||||
|
||||
private val _dialpadIcon = MutableStateFlow(R.drawable.dialpad_off)
|
||||
val dialpadIcon: StateFlow<Int> = _dialpadIcon.asStateFlow()
|
||||
|
||||
fun updateDialpadIcon(newIcon: Int) {
|
||||
_dialpadIcon.value = newIcon
|
||||
}
|
||||
|
||||
private val _audioSettingsResult = mutableStateOf<Boolean?>(null)
|
||||
val audioSettingsResult: State<Boolean?> get() = _audioSettingsResult
|
||||
|
||||
fun setAudioSettingsResult(result: Boolean) {
|
||||
_audioSettingsResult.value = result
|
||||
}
|
||||
|
||||
fun clearAudioSettingsResult() {
|
||||
_audioSettingsResult.value = null
|
||||
}
|
||||
|
||||
private val _selectedCallRow = MutableStateFlow<CallRow?>(null)
|
||||
|
||||
fun selectCallRow(callRow: CallRow) {
|
||||
_selectedCallRow.value = callRow
|
||||
}
|
||||
|
||||
fun consumeSelectedCallRow(): CallRow? {
|
||||
val callRow = _selectedCallRow.value
|
||||
_selectedCallRow.value = null
|
||||
return callRow
|
||||
}
|
||||
|
||||
private val _navigationCommand = MutableSharedFlow<NavigationCommand>()
|
||||
val navigationCommand = _navigationCommand.asSharedFlow()
|
||||
|
||||
fun onNewMessageReceived(aor: String, peer: String) {
|
||||
viewModelScope.launch {
|
||||
_navigationCommand.emit(NavigationCommand.NavigateToChat(aor, peer))
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToCalls(aor: String) {
|
||||
viewModelScope.launch {
|
||||
_navigationCommand.emit(NavigationCommand.NavigateToCalls(aor))
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToHome() {
|
||||
viewModelScope.launch {
|
||||
_navigationCommand.emit(NavigationCommand.NavigateToHome)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
<li>Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.</li>
|
||||
<li>Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.</li>
|
||||
<li>Voit lisätä/poistaa yhteystiedon avatar-kuvan koskettamalla yhteystiedon ikonia lyhyesti/pitkästi.</li>
|
||||
<li>Audio-koodekin saa pitkällä kosketuksella käytöön/pois käytöstä.</li>
|
||||
<li>Katso lisätietoja <a href=https://github.com/juha-h/baresip-studio/wiki>Wiki</a>-sivulta.</li>
|
||||
</ul>
|
||||
<h2>Tietosuoja</h2>
|
||||
@ -69,6 +70,7 @@
|
||||
<li>Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.</li>
|
||||
<li>Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.</li>
|
||||
<li>Voit lisätä/poistaa yhteystiedon avatar-kuvan koskettamalla yhteystiedon ikonia lyhyesti/pitkästi.</li>
|
||||
<li>Audio- ja video-koodekin saa pitkällä kosketuksella käytöön/pois käytöstä.</li>
|
||||
<li>Katso lisätietoja <a href=https://github.com/juha-h/baresip-studio/wiki>Wiki</a>-sivulta.</li>
|
||||
</ul>
|
||||
<h2>Tunnetut ongelmat</h2>
|
||||
@ -255,6 +257,8 @@
|
||||
<!-- Baresip Service -->
|
||||
<string name="answer">Vastaa</string>
|
||||
<string name="reject">Hylkää</string>
|
||||
<string name="reply">Vastaa</string>
|
||||
<string name="save">Talleta</string>
|
||||
<string name="incoming_call_from">Puhelu soittajalta</string>
|
||||
<string name="missed_call_from">Vastaamaton puhelu soittajalta</string>
|
||||
<string name="missed_calls">Vastaamattomia puheluita</string>
|
||||
@ -593,8 +597,13 @@
|
||||
<string name="no_android_contacts">Et voi käyttää Androidin yhteystietoja ilman Yhteystiedot-lupaa.</string>
|
||||
<string name="audio_focus_denied">Audiofokus on evätty!</string>
|
||||
<string name="permissions_rationale">Tarvittavat luvat</string>
|
||||
<string name="audio_permissions">baresip tarvitsee Mikrofoni-luvan puheluita varten ja Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen havaitsemista varten ja Ilmoitukset-luvan ilmoitusten lähettämistä varten.</string>
|
||||
<string name="audio_and_video_permissions">baresip+ tarvitsee Mikrofoni-luvan puheluita varten, Kamera-luvan videopuheluita varten ja Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen havaitsemista varten ja Ilmoitukset-luvan ilmoitusten näyttämistä varten.</string>
|
||||
<string name="audio_permissions">baresip tarvitsee Mikrofoni-luvan puheluita varten, Lähellä olevat laitteet -luvan
|
||||
Bluetooth-mikrofonin/kaiuttimen havaitsemista varten, Ilmoitukset-luvan ilmoitusten lähettämistä varten
|
||||
ja Android versiossa 9 Tallennustila-luvan Talleta/Palauta-toimintoja varten.</string>
|
||||
<string name="audio_and_video_permissions">baresip+ tarvitsee Mikrofoni-luvan puheluita varten,
|
||||
Kamera-luvan videopuheluita varten, Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen
|
||||
havaitsemista varten, Ilmoitukset-luvan ilmoitusten näyttämistä varten ja Android versiossa 9
|
||||
Tallennustila-luvan Talleta/Palauta-toimintoja varten.</string>
|
||||
<string name="call_recording_title">Puheluiden talletus</string>
|
||||
<string name="call_recording_tip">Jos aktivoitu, uudet soitetut ja vastatut puhelut talletetaan.
|
||||
Tallennukset voi kuunnella Puhelutiedot-sivulla.</string>
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
<li>Peers of calls and messages can be added to contacts by long touches.</li>
|
||||
<li>Long touches can also be used to remove calls, chats, messages, and contacts.</li>
|
||||
<li>Touch/long touch on contact icon can be used to install/remove image avatar.</li>
|
||||
<li>Long touch on an audio codec can be used to enable/disable the codec.</li>
|
||||
<li>See <a href="https://github.com/juha-h/baresip-studio/wiki">Wiki</a> for more information.</li>
|
||||
</ul>
|
||||
<h2>Privacy Policy</h2>
|
||||
@ -72,6 +73,7 @@
|
||||
<li>Peers of calls and messages can be added to contacts by long touches.</li>
|
||||
<li>Long touches can also be used to remove calls, chats, messages, and contacts.</li>
|
||||
<li>Touch/long touch of contact icon can be used to install/remove image avatar.</li>
|
||||
<li>Long touch on an audio or video codec can be used to enable/disable the codec.</li>
|
||||
<li>See <a href="https://github.com/juha-h/baresip-studio/wiki">Wiki</a> for more
|
||||
information.</li>
|
||||
</ul>
|
||||
@ -240,6 +242,8 @@
|
||||
<!-- Baresip Service -->
|
||||
<string name="answer">Answer</string>
|
||||
<string name="reject">Reject</string>
|
||||
<string name="reply">Reply</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="incoming_call_from">Incoming call from</string>
|
||||
<string name="missed_call_from">Missed call from</string>
|
||||
<string name="missed_calls">Missed calls</string>
|
||||
@ -575,11 +579,13 @@
|
||||
<string name="audio_focus_denied">Audio focus denied!</string>
|
||||
<string name="permissions_rationale">Permissions rationale</string>
|
||||
<string name="audio_permissions">baresip needs \"Microphone\" permission for voice calls,
|
||||
\"Nearby devices\" permission for Bluetooth microphone/speaker detection, and
|
||||
\"Notifications\" permission for posting notifications.</string>
|
||||
<string name="audio_and_video_permissions">baresip+ needs \"Microphone\" permission for voice calls,
|
||||
\"Camera\" permission for video calls, \"Nearby devices\" permission for Bluetooth
|
||||
microphone/speaker detection, and \"Notifications\" permission for posting notifications.</string>
|
||||
\"Nearby devices\" permission for Bluetooth microphone/speaker detection,
|
||||
\"Notifications\" permission for posting notifications, and in Android 9
|
||||
\"Storage\" permission for Backup/Restore operations.</string>
|
||||
<string name="audio_and_video_permissions">baresip+ needs \"Microphone\" permission
|
||||
for voice calls, \"Camera\" permission for video calls, \"Nearby devices\" permission
|
||||
for Bluetooth microphone/speaker detection, \"Notifications\" permission for posting
|
||||
notifications, and in Android 9 \"Storage\" permissions for Backup/Restore operations.</string>
|
||||
<string name="call_recording_title">Call Recording</string>
|
||||
<string name="call_recording_tip">If activated, new incoming and outgoing calls will be recorded.
|
||||
Recordings can be played on Call Details page</string>
|
||||
@ -587,4 +593,4 @@
|
||||
<string name="microphone_tip">If activated during call, microphone is muted.</string>
|
||||
<string name="speakerphone_title">Speakerphone</string>
|
||||
<string name="speakerphone_tip">If activated, audio is played via device speakerphone.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
Submodule libbaresip-android updated: d244a177e9...75d9f03a02
Reference in New Issue
Block a user