Migration of user interface to Jetpack Compose

This commit is contained in:
Juha Heinanen
2025-03-29 19:20:43 +02:00
parent cd0a4bdd1f
commit f3b2a45d8b
97 changed files with 10597 additions and 8717 deletions

View File

@ -1,79 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdk 35
ndkVersion '27.2.12479018'
defaultConfig {
applicationId = 'com.tutpro.baresip'
minSdkVersion 28
targetSdkVersion 35
versionCode = 416
versionName = '63.3.0'
externalNativeBuild {
cmake {
cFlags '-DHAVE_INTTYPES_H -lstdc++'
arguments '-DANDROID_STL=c++_shared'
version '3.25.1'
}
}
ndk {
// noinspection ChromeOsAbiSupport
abiFilters 'armeabi-v7a', 'arm64-v8a' //, 'x86_64'
}
vectorDrawables.useSupportLibrary = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
splits {
abi {
reset()
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
dataBinding true
buildConfig true
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
}
}
namespace 'com.tutpro.baresip'
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.exifinterface:exifinterface:1.3.7"
implementation "androidx.core:core-ktx:1.15.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.activity:activity-ktx:1.10.0"
implementation "androidx.fragment:fragment-ktx:1.8.6"
implementation 'androidx.media:media:1.7.0'
}

116
app/build.gradle.kts Normal file
View File

@ -0,0 +1,116 @@
plugins {
alias(libs.plugins.compose.compiler)
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
compileSdk = 35
ndkVersion = "27.2.12479018"
defaultConfig {
applicationId = "com.tutpro.baresip"
minSdk = 28
targetSdk = 35
versionCode = 416
versionName = "63.3.0"
externalNativeBuild {
cmake {
cFlags += "-DHAVE_INTTYPES_H -lstdc++"
arguments.addAll(listOf("-DANDROID_STL=c++_shared"))
version = "3.25.1"
}
}
ndk {
// noinspection ChromeOsAbiSupport
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86_64"))
}
vectorDrawables.useSupportLibrary = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.7"
}
splits {
abi {
reset()
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
viewBinding = true
//noinspection DataBindingWithoutKapt
dataBinding = true
buildConfig = true
compose = true
}
sourceSets {
getByName("main").java.srcDirs("src/main/kotlin")
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
namespace = "com.tutpro.baresip"
}
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
// stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}
dependencies {
implementation(libs.androidx.foundation.android)
implementation(libs.androidx.tools.core)
implementation(libs.androidx.runtime.livedata)
implementation(libs.androidx.ui.test.android)
implementation(libs.androidx.foundation.android)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
val composeBom = libs.androidx.compose.bom
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.foundation)
implementation(libs.androidx.ui)
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.material)
implementation(libs.androidx.cardview)
implementation(libs.androidx.localbroadcastmanager)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.exifinterface)
implementation(libs.androidx.core.ktx)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.extensions)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.media)
implementation(libs.coil.compose)
implementation(libs.ui.tooling.preview)
debugImplementation(libs.ui.tooling)
}

View File

@ -45,7 +45,7 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:largeHeap="true"
android:theme="@style/AppTheme"
android:theme="@style/Theme.Material3.DayNight.NoActionBar"
tools:remove="android:appComponentFactory" >
<activity
@ -53,7 +53,6 @@
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/AppTheme.Main"
android:windowSoftInputMode="adjustResize" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -78,7 +77,7 @@
<activity
android:name=".AccountsActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustPan"
android:windowSoftInputMode="adjustResize"
android:label="@string/accounts"
android:parentActivityName=".MainActivity" >
</activity>
@ -86,7 +85,7 @@
<activity
android:name=".AccountActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustPan"
android:windowSoftInputMode="adjustResize"
android:label="@string/account" >
</activity>
@ -94,6 +93,7 @@
android:name=".CodecsActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/codecs"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".AccountActivity" >
</activity>
@ -101,12 +101,14 @@
android:name=".ContactsActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/contacts"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" >
</activity>
<activity
android:name=".ContactActivity"
android:name=".BaresipContactActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustResize"
android:label="@string/contact"
android:parentActivityName=".ContactsActivity" >
</activity>
@ -115,6 +117,7 @@
android:name=".AndroidContactActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/contact"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".ContactsActivity" >
</activity>
@ -122,6 +125,7 @@
android:name=".ConfigActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/configuration"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" >
</activity>
@ -129,6 +133,7 @@
android:name=".AudioActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/audio_settings"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".ConfigActivity" >
</activity>
@ -136,6 +141,7 @@
android:name=".CallsActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/call_history"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" >
</activity>
@ -143,19 +149,21 @@
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="adjustPan"
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>
@ -164,6 +172,7 @@
android:name=".AboutActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/about_title"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" >
</activity>

View File

@ -1799,10 +1799,11 @@ JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_module_1unload(
(*env)->ReleaseStringUTFChars(env, javaModule, native_module);
}
AAudioStream *AAudio_stream = NULL;
JNIEXPORT jint JNICALL
Java_com_tutpro_baresip_Api_create_1AAudio_1SessionId(JNIEnv *env, jobject obj) {
Java_com_tutpro_baresip_Api_AAudio_1open_1stream(JNIEnv *env, jobject obj) {
AAudioStreamBuilder *builder = NULL;
AAudioStream *stream = NULL;
jint sessionId = -1;
if (AAudio_createStreamBuilder(&builder) != AAUDIO_OK) {
@ -1819,15 +1820,22 @@ Java_com_tutpro_baresip_Api_create_1AAudio_1SessionId(JNIEnv *env, jobject obj)
AAudioStreamBuilder_setChannelCount(builder, 1);
AAudioStreamBuilder_setSessionId(builder, AAUDIO_SESSION_ID_ALLOCATE);
if (AAudioStreamBuilder_openStream(builder, &stream) == AAUDIO_OK) {
sessionId = AAudioStream_getSessionId(stream);
struct timespec td = {
.tv_nsec = 100*1000*1000 /* 100ms */
};
nanosleep(&td, NULL);
AAudioStream_close(stream);
if (AAudioStreamBuilder_openStream(builder, &AAudio_stream) == AAUDIO_OK)
sessionId = AAudioStream_getSessionId(AAudio_stream);
else {
LOGE("Failed to open AAudio stream\n");
AAudio_stream = NULL;
}
AAudioStreamBuilder_delete(builder);
return sessionId;
}
JNIEXPORT void JNICALL
Java_com_tutpro_baresip_Api_AAudio_1close_1stream(JNIEnv *env, jobject obj)
{
if (AAudio_stream != NULL) {
AAudioStream_close(AAudio_stream);
AAudio_stream = NULL;
}
}

View File

@ -1,25 +1,43 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.text.HtmlCompat
import androidx.appcompat.app.AppCompatActivity
import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import com.tutpro.baresip.databinding.ActivityAboutBinding
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
class AboutActivity : AppCompatActivity() {
private lateinit var binding: ActivityAboutBinding
class AboutActivity : ComponentActivity() {
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@ -30,31 +48,78 @@ class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.aboutText.updatePadding(top = 172)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
enableEdgeToEdge()
Utils.addActivity("about")
val text = String.format(getString(R.string.about_text),
BuildConfig.VERSION_NAME)
binding.aboutText.text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)
binding.aboutText.movementMethod = LinkMovementMethod.getInstance()
val aboutTitle = String.format(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() }
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@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(MaterialTheme.colorScheme.background)
)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
@ -67,9 +132,8 @@ class AboutActivity : AppCompatActivity() {
private fun goBack() {
BaresipService.activities.remove("about")
setResult(Activity.RESULT_CANCELED, Intent())
setResult(RESULT_CANCELED, Intent())
finish()
}
}

View File

@ -1,6 +1,8 @@
package com.tutpro.baresip
import android.content.Context
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import java.util.*
import kotlin.collections.ArrayList
import java.net.URLEncoder
@ -8,10 +10,10 @@ import java.net.URLDecoder
class Account(val accp: Long) {
var nickName = ""
val nickName: MutableState<String> = mutableStateOf("")
var displayName = Api.account_display_name(accp)
val aor = Api.account_aor(accp)
var luri = Api.account_luri(accp)
private var luri = Api.account_luri(accp)
var authUser = Api.account_auth_user(accp)
var authPass = Api.account_auth_pass(accp)
var outbound = ArrayList<String>()
@ -68,7 +70,7 @@ class Account(val accp: Long) {
val extra = Api.account_extra(accp)
if (Utils.paramExists(extra, "nickname"))
nickName = Utils.paramValue(extra,"nickname")
nickName.value = Utils.paramValue(extra,"nickname")
if (Utils.paramExists(extra, "regint"))
configuredRegInt = Utils.paramValue(extra,"regint").toInt()
callHistory = Utils.paramValue(extra,"call_history") == ""
@ -151,8 +153,8 @@ class Account(val accp: Long) {
var extra = ""
if (nickName != "")
extra += ";nickname=$nickName"
if (nickName.value != "")
extra += ";nickname=${nickName.value}"
if (!callHistory)
extra += ";call_history=no"
@ -207,6 +209,13 @@ class Account(val accp: Long) {
return aor.split("@")[1]
}
fun text(): String {
return if (nickName.value != "")
nickName.value
else
aor.split(":")[1].substringBefore(";")
}
private fun removeAudioCodecsStartingWith(prefix: String) {
val newCodecs = ArrayList<String>()
for (acSpec in audioCodec)
@ -229,14 +238,14 @@ class Account(val accp: Long) {
fun accounts(): ArrayList<Account> {
val res = ArrayList<Account>()
for (ua in BaresipService.uas) {
for (ua in BaresipService.uas.value) {
res.add(ua.account)
}
return res
}
fun ofAor(aor: String): Account? {
for (ua in BaresipService.uas)
for (ua in BaresipService.uas.value)
if (ua.account.aor == aor) return ua.account
return null
}
@ -263,8 +272,8 @@ class Account(val accp: Long) {
}
fun uniqueNickName(nickName: String): Boolean {
for (ua in BaresipService.uas)
if (ua.account.nickName == nickName)
for (ua in BaresipService.uas.value)
if (ua.account.nickName.value == nickName)
return false
return true
}

File diff suppressed because it is too large Load Diff

View File

@ -1,80 +0,0 @@
package com.tutpro.baresip
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.*
class AccountListAdapter(private val cxt: Context, private val rows: ArrayList<AccountRow>) :
ArrayAdapter<AccountRow>(cxt, R.layout.account_row, rows) {
private val layoutInflater = LayoutInflater.from(context)
private class ViewHolder(view: View?) {
val aorView: TextView = view?.findViewById(R.id.aor)!!
val actionView: ImageButton = view?.findViewById(R.id.action)!!
}
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
val ua = BaresipService.uas[position]
if (view == null) {
rowView = layoutInflater.inflate(R.layout.account_row, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val row = rows[position]
viewHolder.aorView.text = row.aor.split(":")[0]
viewHolder.aorView.textSize = 20f
viewHolder.aorView.setPadding(6, 6, 0, 6)
viewHolder.actionView.setImageResource(row.action)
viewHolder.aorView.setOnClickListener {
val i = Intent(cxt, AccountActivity::class.java)
val b = Bundle()
b.putString("aor", BaresipService.uas[position].account.aor)
i.putExtras(b)
MainActivity.accountRequest!!.launch(i)
}
viewHolder.actionView.setOnClickListener {
with (MaterialAlertDialogBuilder(cxt, R.style.AlertDialogTheme)) {
setTitle(R.string.confirmation)
setMessage(String.format(cxt.getString(R.string.delete_account),
viewHolder.aorView.text))
setPositiveButton(cxt.getText(R.string.delete)) { dialog, _ ->
Api.ua_destroy(ua.uap)
CallHistoryNew.clear(ua.account.aor)
Message.clear(ua.account.aor)
ua.remove()
AccountsActivity.generateAccounts()
AccountsActivity.saveAccounts()
this@AccountListAdapter.notifyDataSetChanged()
dialog.dismiss()
}
setNeutralButton(cxt.getText(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
show()
}
}
return rowView
}
}

View File

@ -1,3 +0,0 @@
package com.tutpro.baresip
class AccountRow(val aor: String, val action: Int)

View File

@ -1,27 +1,84 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.os.SystemClock
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import com.tutpro.baresip.databinding.ActivityAccountsBinding
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
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.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tutpro.baresip.CustomElements.verticalScrollbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
import java.io.StringReader
import java.net.URL
import java.util.Locale
class AccountsActivity : AppCompatActivity() {
class AccountsActivity : ComponentActivity() {
private lateinit var binding: ActivityAccountsBinding
private lateinit var alAdapter: AccountListAdapter
internal lateinit var aor: String
private lateinit var mediaEncMap: Map<String, String>
private lateinit var mediaNatMap: Map<String, String>
private var lastClick: Long = 0
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@ -32,106 +89,380 @@ class AccountsActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAccountsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.AccountsView.updatePadding(top = 172)
WindowInsetsCompat.CONSUMED
}
enableEdgeToEdge()
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
val title = String.format(getString(R.string.accounts))
aor = intent.getStringExtra("aor")!!
Utils.addActivity("accounts,$aor")
val listView = binding.accounts
generateAccounts()
alAdapter = AccountListAdapter(this, accounts)
listView.adapter = alAdapter
mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF",
"srtp-mand" to "SRTP-MAND", "srtp" to "SRTP", "" to "--")
val accountRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
val addAccountButton = binding.addAccount
val newAorView = binding.newAor
addAccountButton.setOnClickListener {
val aor = newAorView.text.toString().trim()
if (!Utils.checkAor("sip:$aor")) {
Log.d(TAG, "Invalid Address of Record $aor")
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.invalid_aor), aor))
return@setOnClickListener
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
AccountsScreen(this, title) { goBack() }
}
}
if (Account.ofAor("sip:$aor") != null) {
Log.d(TAG, "Account $aor already exists")
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.account_exists), aor.split(":")[0]))
return@setOnClickListener
}
val laddr = "sip:$aor"
val ua = UserAgent.uaAlloc("<$laddr>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;mwi=no")
if (ua == null) {
Log.e(TAG, "Failed to allocate UA for $aor")
Utils.alertView(this, getString(R.string.notice),
getString(R.string.account_allocation_failure))
return@setOnClickListener
}
// Api.account_debug(ua.account.accp)
Log.d(TAG, "Allocated UA ${ua.uap} for ${Api.account_luri(ua.account.accp)}")
newAorView.setText("")
newAorView.hint = getString(R.string.user_domain)
newAorView.clearFocus()
generateAccounts()
alAdapter.notifyDataSetChanged()
saveAccounts()
val i = Intent(this, AccountActivity::class.java)
val b = Bundle()
b.putString("aor", ua.account.aor)
b.putBoolean("new", true)
i.putExtras(b)
accountRequest.launch(i)
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (BaresipService.activities.indexOf("accounts,$aor") == -1)
return true
when (item.itemId) {
R.id.help -> {
Utils.alertView(this@AccountsActivity, getString(R.string.new_account),
getString(R.string.accounts_help))
return true
}
android.R.id.home -> {
goBack()
return true
}
}
return super.onOptionsItemSelected(item)
@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)
},
)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.accounts_menu, menu)
return true
@Composable
fun AccountsContent(ctx: Context, contentPadding: PaddingValues) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(LocalCustomColors.current.background)
.padding(contentPadding)
.padding(top = 8.dp),
verticalArrangement = Arrangement.Top
) {
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier.fillMaxWidth()
.imePadding()
.fillMaxWidth()
.padding(start = 16.dp, end = 4.dp)
.verticalScrollbar(
state = lazyListState,
width = 4.dp,
color = LocalCustomColors.current.gray
),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(BaresipService.uas.value, key = { it.account.aor }) { ua ->
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()
with(
MaterialAlertDialogBuilder(
ctx,
R.style.AlertDialogTheme
)
) {
setTitle(R.string.confirmation)
setMessage(
String.format(
ctx.getString(R.string.delete_account),
text
)
)
setPositiveButton(R.string.delete) { dialog, _ ->
CallHistoryNew.clear(aor)
Message.clearMessagesOfAor(aor)
ua.remove()
Api.ua_destroy(ua.uap)
saveAccounts()
dialog.dismiss()
}
setNeutralButton(ctx.getText(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
show()
}
}
},
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 = { CustomElements.Text(text = getString(R.string.new_account)) },
onValueChange = { newAor = it },
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
.verticalScroll(rememberScrollState())
.clickable {
Utils.alertView(ctx, getString(R.string.new_account),
getString(R.string.accounts_help)) },
singleLine = false,
trailingIcon = {
if (newAor.isNotEmpty()) {
Icon(
Icons.Outlined.Clear,
contentDescription = "Clear",
modifier = Modifier.clickable { newAor = "" }
)
}
},
label = {
CustomElements.Text(
text = getString(R.string.new_account),
fontSize = 18.sp
)
},
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)
i.putExtras(b)
startActivity(i)
newAor = ""
focusManager.clearFocus()
}
}
),
)
}
}
private fun createNew(newAor: String): Account? {
val aor = if (newAor.startsWith("sip:"))
newAor
else
"sip:$newAor"
if (!Utils.checkAor(aor)) {
Log.d(TAG, "Invalid Address of Record $aor")
Utils.alertView(
this, getString(R.string.notice),
String.format(getString(R.string.invalid_aor),
aor.split(":")[1])
)
return null
}
if (Account.ofAor(aor) != null) {
Log.d(TAG, "Account $aor already exists")
Utils.alertView(
this, getString(R.string.notice),
String.format(getString(R.string.account_exists),
aor.split(":")[1])
)
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) {
Log.e(TAG, "Failed to allocate UA for $aor")
Utils.alertView(
this, getString(R.string.notice),
getString(R.string.account_allocation_failure)
)
return null
}
// Api.account_debug(ua.account.accp)
val acc = ua.account
Log.d(TAG, "Allocated UA ${ua.uap} for ${Api.account_luri(acc.accp)}")
initAccountFromConfig(this@AccountsActivity, acc)
saveAccounts()
return acc
}
private fun initAccountFromConfig(ctx: AccountsActivity, acc: Account) {
scope.launch(Dispatchers.IO) {
val url = "https://${Utils.uriHostPart(acc.aor)}/baresip/account_config.xml"
val config = try {
URL(url).readText()
} catch (e: java.lang.Exception) {
Log.d(TAG, "Failed to get account configuration from network")
null
}
if (config != null && !ctx.isFinishing) {
Log.d(TAG, "Got account config '$config'")
val parserFactory: XmlPullParserFactory = XmlPullParserFactory.newInstance()
val parser: XmlPullParser = parserFactory.newPullParser()
parser.setInput(StringReader(config))
var tag: String?
var text = ""
var event = parser.eventType
val audioCodecs = ArrayList(Api.audio_codecs().split(","))
val videoCodecs = ArrayList(Api.video_codecs().split(","))
while (event != XmlPullParser.END_DOCUMENT) {
tag = parser.name
when (event) {
XmlPullParser.TEXT ->
text = parser.text
XmlPullParser.START_TAG -> {
if (tag == "audio-codecs")
acc.audioCodec.clear()
if (tag == "video-codecs")
acc.videoCodec.clear()
}
XmlPullParser.END_TAG ->
when (tag) {
"outbound-proxy-1" ->
if (text.isNotEmpty())
acc.outbound.add(text)
"outbound-proxy-2" ->
if (text.isNotEmpty())
acc.outbound.add(text)
"registration-interval" ->
acc.configuredRegInt = text.toInt()
"register" ->
acc.regint = if (text == "yes") acc.configuredRegInt else 0
"audio-codec" ->
if (text in audioCodecs)
acc.audioCodec.add(text)
"video-codec" ->
if (text in videoCodecs)
acc.videoCodec.add(text)
"media-encoding" -> {
val enc = text.lowercase(Locale.ROOT)
if (enc in mediaEncMap.keys && enc.isNotEmpty())
acc.mediaEnc = enc
}
"media-nat" -> {
val nat = text.lowercase(Locale.ROOT)
if (nat in mediaNatMap.keys && nat.isNotEmpty())
acc.mediaNat = nat
}
"stun-turn-server" ->
if (text.isNotEmpty())
acc.stunServer = text
"rtcp-mux" ->
acc.rtcpMux = text == "yes"
"100rel-mode" ->
acc.rel100Mode = if (text == "yes")
Api.REL100_ENABLED
else
Api.REL100_DISABLED
"dtmf-mode" ->
if (text in arrayOf("rtp-event", "sip-info", "auto"))
acc.dtmfMode = when (text) {
"rtp-event" -> Api.DTMFMODE_RTP_EVENT
"sip-info" -> Api.DTMFMODE_SIP_INFO
else -> Api.DTMFMODE_AUTO
}
"answer-mode" ->
if (text in arrayOf("manual", "auto"))
acc.answerMode = if (text == "manual")
Api.ANSWERMODE_MANUAL
else
Api.ANSWERMODE_AUTO
"redirect-mode" ->
acc.autoRedirect = text == "yes"
"voicemail-uri" ->
if (text.isNotEmpty())
acc.vmUri = text
"country-code" ->
acc.countryCode = text
"tel-provider" ->
acc.telProvider = text
}
}
event = parser.next()
}
}
}
}
private fun goBack() {
BaresipService.activities.remove("accounts,$aor")
setResult(Activity.RESULT_CANCELED, Intent())
setResult(RESULT_CANCELED, Intent())
finish()
}
@ -142,19 +473,11 @@ class AccountsActivity : AppCompatActivity() {
companion object {
var accounts = ArrayList<AccountRow>()
fun generateAccounts() {
accounts.clear()
for (ua in BaresipService.uas)
accounts.add(AccountRow(ua.account.aor.replace("sip:", ""),
R.drawable.delete))
}
fun saveAccounts() {
var accounts = ""
for (a in Account.accounts()) accounts = accounts + a.print() + "\n"
Utils.putFileContents(BaresipService.filesPath + "/accounts", accounts.toByteArray(Charsets.UTF_8))
Utils.putFileContents(BaresipService.filesPath + "/accounts",
accounts.toByteArray(Charsets.UTF_8))
// Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
}

View File

@ -1,31 +1,64 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import com.tutpro.baresip.databinding.ActivityAndroidContactBinding
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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 : AppCompatActivity() {
class AndroidContactActivity : ComponentActivity() {
private lateinit var binding: ActivityAndroidContactBinding
private lateinit var layout: LinearLayout
private lateinit var textAvatarView: TextView
private lateinit var cardAvatarView: CardView
private lateinit var cardImageAvatarView: ImageView
private lateinit var nameView: TextView
private lateinit var ulAdapter: AndroidUriListAdapter
private lateinit var aor: String
private lateinit var name: String
private var index = 0
private var color = 0
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
@ -37,70 +70,207 @@ class AndroidContactActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAndroidContactBinding.inflate(layoutInflater)
setContentView(binding.root)
layout = binding.ContactView
enableEdgeToEdge()
aor = intent.getStringExtra("aor")!!
index = intent.getIntExtra("index", 0)
name = intent.getStringExtra("name")!!
textAvatarView = binding.TextAvatar
cardAvatarView = binding.CardAvatar
cardImageAvatarView = binding.ImageAvatar
nameView = binding.Name
val title: String = name
val contact = Contact.androidContact(name)
val contact = Contact.contacts()[index] as Contact.AndroidContact
val name = contact.name
color = contact.color
val thumbnailUri = contact.thumbnailUri
if (thumbnailUri != null)
showImageAvatar(thumbnailUri)
else
showTextAvatar(name, color)
title = name
nameView.text = name
val listView = binding.uris
ulAdapter = AndroidUriListAdapter(this, contact.uris, aor)
listView.adapter = ulAdapter
Utils.addActivity("android contact,$aor,$index")
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (BaresipService.activities.indexOf("android contact,$aor,$index") == -1)
return true
when (item.itemId) {
android.R.id.home ->
goBack()
if (contact == null) {
Log.e(TAG, "No Android contact found with name $name")
goBack()
}
return true
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
ContactScreen(this, title, contact!!) { goBack() }
}
}
}
Utils.addActivity("android contact, $name")
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@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)
}
}
)
}
}
}
}
private fun goBack() {
BaresipService.activities.remove("android contact,$index")
setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java))
BaresipService.activities.remove("android contact,$name")
setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
finish()
}
private fun showTextAvatar(name: String, color: Int) {
textAvatarView.visibility = View.VISIBLE
cardAvatarView.visibility = View.GONE
textAvatarView.background.setTint(color)
textAvatarView.text = "${name[0]}"
}
private fun showImageAvatar(thumbNailUri: Uri) {
textAvatarView.visibility = View.GONE
cardAvatarView.visibility = View.VISIBLE
cardImageAvatarView.setImageURI(thumbNailUri)
}
}

View File

@ -1,76 +0,0 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.TextView
class AndroidUriListAdapter(private val ctx: Context, private val rows: ArrayList<String>, val aor: String) :
ArrayAdapter<String>(ctx, R.layout.android_uri_row, rows) {
private val layoutInflater = LayoutInflater.from(context)
private class ViewHolder(view: View?) {
val uriView: TextView = view?.findViewById(R.id.uri)!!
val messageView: ImageButton = view?.findViewById(R.id.message)!!
val callView: ImageButton = view?.findViewById(R.id.call)!!
}
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
if (view == null) {
rowView = layoutInflater.inflate(R.layout.android_uri_row, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val uri = rows[position]
viewHolder.uriView.text = uri.substringAfter(":")
viewHolder.messageView.setOnClickListener {
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", uri)
(ctx as Activity).startActivity(i)
}
}
viewHolder.callView.setOnClickListener {
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", uri)
(ctx as Activity).startActivity(i)
}
}
return rowView
}
}

View File

@ -121,6 +121,6 @@ object Api {
external fun module_load(module: String): Int
external fun module_unload(module: String)
external fun create_AAudio_SessionId(): Int
external fun AAudio_open_stream(): Int
external fun AAudio_close_stream()
}

View File

@ -0,0 +1,67 @@
package com.tutpro.baresip
import android.app.Activity
import android.os.Build
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
@Composable
fun AppTheme(
content: @Composable () -> Unit
) {
val darkTheme = 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)
}
darkTheme.value -> darkColorScheme()
else -> lightColorScheme()
}
// logic for which custom palette to use
val customColorsPalette =
if (darkTheme.value) DarkCustomColors
else LightCustomColors
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
val decorView = window.decorView
// Ensure insets are applied correctly
WindowCompat.setDecorFitsSystemWindows(window, false)
// Handle the status bar appearance
val insetsController = WindowCompat.getInsetsController(window, decorView)
insetsController.apply {
isAppearanceLightStatusBars = !darkTheme.value
isAppearanceLightNavigationBars = !darkTheme.value
}
}
}
// here is the important point, where you will expose custom objects
CompositionLocalProvider(
LocalCustomColors provides customColorsPalette // our custom palette
) {
MaterialTheme(
colorScheme = colorScheme, // the MaterialTheme still uses the "normal" palette
content = content
)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,704 @@
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.Bundle
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds
import android.provider.ContactsContract.Contacts.Data
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.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.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.draw.clip
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.Checkbox
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 avatarImage: Bitmap? = null
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 newAvatar = ""
private var tmpFile: File? = null
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
goBack()
}
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val title: String
new = intent.getBooleanExtra("new", false)
if (new) {
name = ""
uri = intent.getStringExtra("uri")!!
favorite = false
avatarImage = null
android = BaresipService.contactsMode == "android"
title = getString(R.string.new_contact)
uriOrName = uri
color = Utils.randomColor()
id = System.currentTimeMillis()
} else {
name = intent.getStringExtra("name")!!
val contact = Contact.baresipContact(name)!!
uri = contact.uri
favorite = contact.favorite
avatarImage = contact.avatarImage
android = false
title = name
uriOrName = name
color = contact.color
id = contact.id
}
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
ContactScreen(this, title) { goBack() }
}
}
}
Utils.addActivity("baresip contact,$new,$uriOrName")
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@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(ctx, contentPadding)
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBar(ctx: Context, title: String, navigateBack: () -> Unit) {
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
)
}
},
actions = {
IconButton(onClick = {
checkOnClick(ctx)
}) {
Icon(
imageVector = Icons.Filled.Check,
tint = LocalCustomColors.current.light,
contentDescription = "Check"
)
}
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactContent(ctx: Context, contentPadding: PaddingValues) {
var avatarType by remember { mutableStateOf(if (avatarImage != null) "image" else "text") }
var textAvatarText by remember { mutableStateOf("") }
var textAvatarColor by remember { mutableIntStateOf(0) }
var imageAvatarUri by remember { mutableStateOf("") }
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)
tmpFile = File(
BaresipService.filesPath + "/tmp",
"${System.currentTimeMillis()}.png"
)
if (Utils.saveBitmap(rotatedBitmap, tmpFile!!)) {
newAvatar = "image"
avatarType = newAvatar
imageAvatarUri = Uri.fromFile(tmpFile).toString()
}
} catch (e: Exception) {
Log.e(TAG, "Could not read avatar image: $e")
}
}
@Composable
fun TextAvatar(size: Int) {
Box(
modifier = Modifier.size(size.dp),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(SolidColor(Color(textAvatarColor)))
}
textAvatarText = if (name == "") "" else name[0].toString()
Text(textAvatarText, fontSize = 72.sp, color = Color.White)
}
}
@Composable
fun ImageAvatar(size: Int) {
Image(
painter = rememberAsyncImagePainter(model = imageAvatarUri),
contentDescription = stringResource(R.string.avatar_image),
contentScale = ContentScale.Crop,
modifier = Modifier.size(size.dp).clip(CircleShape)
)
}
@Composable
fun avatar() {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
if (new) {
textAvatarText = "?"
textAvatarColor = color
} else {
val avatarImage = avatarImage
val avatarFile = File(BaresipService.filesPath, "${id}.png")
if (avatarImage != null && (newAvatar == "" || newAvatar == "image")) {
imageAvatarUri = if (tmpFile != null && tmpFile!!.exists())
Uri.fromFile(tmpFile).toString()
else
Uri.fromFile(avatarFile).toString()
}
else {
textAvatarText = "${name[0]}"
textAvatarColor = color
}
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(96.dp)
.combinedClickable(
onClick = {
avatarRequest.launch("image/*")
},
onLongClick = {
textAvatarColor = Utils.randomColor()
color = textAvatarColor
newAvatar = "text"
avatarType = newAvatar
}
),
) {
if (avatarType == "text")
TextAvatar(96)
else
ImageAvatar(96)
}
}
}
@Composable
fun contactName() {
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(),
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.Sentences
)
)
}
@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(ctx: Context) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Text(
text = stringResource(R.string.favorite),
modifier = Modifier.weight(1f)
.clickable {
Utils.alertView(
ctx, getString(R.string.favorite), getString(R.string.favorite_help)
)
},
color = LocalCustomColors.current.itemText,
)
var favoriteContact by remember { mutableStateOf(favorite) }
newFavorite = favoriteContact
Checkbox(
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
Checkbox(
checked = androidContact,
onCheckedChange = {
if (BaresipService.contactsMode != "android") {
androidContact = it
newAndroid = 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()
contactName()
contactUri()
favorite(ctx)
android()
}
}
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)) {
Utils.alertView(
ctx, getString(R.string.notice),
String.format(getString(R.string.invalid_sip_or_tel_uri), newUri)
)
return
}
newName = newName.trim()
if (newName == "") newName = newUri.substringAfter(":")
if (!Utils.checkName(newName)) {
Utils.alertView(
ctx, getString(R.string.notice),
String.format(getString(R.string.invalid_contact), newName)
)
return
}
val alert: Boolean = if (new)
Contact.nameExists(newName, BaresipService.contacts,true)
else {
(uriOrName != newName) && Contact.nameExists(newName, BaresipService.contacts, false)
}
if (alert) {
Utils.alertView(
ctx, getString(R.string.notice),
String.format(getString(R.string.contact_already_exists), newName))
return
}
val contact: Contact.BaresipContact
if (new) {
contact = Contact.BaresipContact(newName, newUri, color, id, newFavorite)
} else {
contact = Contact.baresipContact(name)!!
contact.uri = newUri
contact.name = newName
contact.color = color
contact.favorite = newFavorite
}
when (newAvatar) {
"text" -> {
if (contact.avatarImage != null) {
contact.avatarImage = null
Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png"))
}
}
"image" -> {
val imageFilePath = BaresipService.filesPath + "/${contact.id}.png"
val imageFile = File(imageFilePath)
tmpFile!!.copyTo(target = imageFile, overwrite = true)
contact.avatarImage = BitmapFactory.decodeFile(imageFilePath)
}
}
if (newAndroid)
addOrUpdateAndroidContact(ctx, contact)
else {
if (new)
Contact.addBaresipContact(contact)
else
Contact.updateBaresipContact(contact)
}
Contact.contactsUpdate()
Contact.saveBaresipContacts()
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")
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")
}
}
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")
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")
}
}
}
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")
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")
null
}
}
private fun goBack() {
BaresipService.activities.remove("baresip contact,$new,$uriOrName")
setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
finish()
}
}

View File

@ -2,7 +2,11 @@ package com.tutpro.baresip
import android.Manifest
import android.annotation.SuppressLint
import android.app.*
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager
@ -13,20 +17,37 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
import android.content.res.Configuration
import android.database.ContentObserver
import android.media.*
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.MODE_IN_COMMUNICATION
import android.media.AudioManager.MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT
import android.media.MediaPlayer
import android.media.Ringtone
import android.media.RingtoneManager
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.AutomaticGainControl
import android.media.audiofx.NoiseSuppressor
import android.net.*
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.*
import android.os.Build.VERSION
import android.os.CountDownTimer
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.provider.ContactsContract
import android.provider.Settings
import android.system.OsConstants
import android.telecom.TelecomManager
import android.text.Spannable
import android.text.SpannableString
@ -37,7 +58,10 @@ import android.widget.Toast
import androidx.annotation.ColorRes
import androidx.annotation.Keep
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
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.content.ContextCompat
@ -49,7 +73,9 @@ import java.io.File
import java.net.InetAddress
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.GregorianCalendar
import java.util.Timer
import java.util.TimerTask
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
@ -99,7 +125,7 @@ class BaresipService: Service() {
filesPath = filesDir.absolutePath
pName = packageName
am = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am = applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager
val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext,
RingtoneManager.TYPE_NOTIFICATION)
@ -113,14 +139,14 @@ class BaresipService: Service() {
createNotificationChannels()
snb = NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
pm = getSystemService(Context.POWER_SERVICE) as PowerManager
pm = getSystemService(POWER_SERVICE) as PowerManager
vibrator = if (VERSION.SDK_INT >= 31) {
val vibratorManager = applicationContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
val vibratorManager = applicationContext.getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
applicationContext.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator
applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator
}
// This is needed to keep service running also in Doze Mode
@ -133,7 +159,7 @@ class BaresipService: Service() {
}
}
cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val builder = NetworkRequest.Builder()
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
cm.registerNetworkCallback(
@ -182,7 +208,7 @@ class BaresipService: Service() {
}
)
wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
wm = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
hotSpotIsEnabled = Utils.isHotSpotOn(wm)
hotSpotReceiver = object : BroadcastReceiver() {
@ -238,9 +264,9 @@ class BaresipService: Service() {
this.registerReceiver(hotSpotReceiver,
IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED"))
tm = getSystemService(Context.TELECOM_SERVICE) as TelecomManager
tm = getSystemService(TELECOM_SERVICE) as TelecomManager
btm = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
btAdapter = btm.adapter
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
@ -530,10 +556,7 @@ class BaresipService: Service() {
if (ua == null)
Log.w(TAG, "onStartCommand did not find UA $uap")
else
ChatsActivity.saveUaMessage(
ua.account.aor,
intent.getStringExtra("time")!!.toLong()
)
Message.updateAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
nm.cancel(MESSAGE_NOTIFICATION_ID)
}
@ -543,10 +566,8 @@ class BaresipService: Service() {
if (ua == null)
Log.w(TAG, "onStartCommand did not find UA $uap")
else
ChatsActivity.deleteUaMessage(
ua.account.aor,
intent.getStringExtra("time")!!.toLong()
)
Message.deleteAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
nm.cancel(MESSAGE_NOTIFICATION_ID)
}
@ -571,6 +592,20 @@ class BaresipService: Service() {
return START_STICKY
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> {
darkTheme.value = Preferences(applicationContext).displayTheme ==
AppCompatDelegate.MODE_NIGHT_YES
}
Configuration.UI_MODE_NIGHT_YES -> {
darkTheme.value = true
}
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
@ -598,7 +633,7 @@ class BaresipService: Service() {
R.drawable.circle_white
else
R.drawable.circle_yellow
uas.add(ua)
ua.add()
val acc = ua.account
if (acc.authUser != "" && acc.authPass == NO_AUTH_PASS) {
@ -629,6 +664,47 @@ class BaresipService: Service() {
if (ev[0] == "recorder sessionid") {
recorderSessionId = ev[1].toInt()
Log.d(TAG, "got recorder sessionid $recorderSessionId")
if (recorderSessionId != 0) {
if (!webrtcAec) {
aec = AcousticEchoCanceler.create(recorderSessionId)
if (aec != null) {
if (!aec!!.getEnabled()) {
aec!!.setEnabled(true)
if (aec!!.getEnabled())
Log.d(TAG, "AEC is enabled")
else
Log.w(TAG, "Failed to enable AEC")
}
} else
Log.w(
TAG, "Failed to create AEC for session " +
"$recorderSessionId"
)
}
if (agcAvailable) {
agc = AutomaticGainControl.create(recorderSessionId)
if (agc != null) {
if (!agc!!.getEnabled()) {
agc!!.setEnabled(true)
if (agc!!.getEnabled())
Log.d(TAG, "AGC is enabled")
}
} else
Log.w(TAG, "Failed to create AGC")
}
if (nsAvailable) {
ns = NoiseSuppressor.create(recorderSessionId)
if (ns != null) {
if (!ns!!.getEnabled()) {
ns!!.setEnabled(true)
if (ns!!.getEnabled())
Log.d(TAG, "NS is enabled")
}
} else
Log.w(TAG, "Failed to create NS")
}
recorderSessionId = 0
}
return
}
@ -649,31 +725,32 @@ class BaresipService: Service() {
return
}
for (accountIndex in uas.indices) {
if (uas[accountIndex].account.aor == aor) {
for (accountIndex in uas.value.indices) {
if (uas.value[accountIndex].account.aor == aor) {
when (ev[0]) {
"registering", "unregistering" -> {
ua.status = R.drawable.circle_yellow
updateStatusNotification()
ua.uaUpdateStatus(R.drawable.circle_yellow)
if (isMainVisible)
registrationUpdate.postValue(System.currentTimeMillis())
return
}
"registered" -> {
ua.status = if (Api.account_regint(ua.account.accp) == 0)
R.drawable.circle_white
else
R.drawable.circle_green
ua.uaUpdateStatus(
if (Api.account_regint(ua.account.accp) == 0)
R.drawable.circle_white
else
R.drawable.circle_green)
updateStatusNotification()
if (isMainVisible)
registrationUpdate.postValue(System.currentTimeMillis())
return
}
"registering failed" -> {
ua.status = if (Api.account_regint(ua.account.accp) == 0)
ua.uaUpdateStatus(if (Api.account_regint(ua.account.accp) == 0)
R.drawable.circle_white
else
R.drawable.circle_red
R.drawable.circle_red)
updateStatusNotification()
if (isMainVisible)
registrationUpdate.postValue(System.currentTimeMillis())
@ -752,7 +829,7 @@ class BaresipService: Service() {
"call incoming" -> {
val peerUri = ev[1]
Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
Call(callp, ua, peerUri, "in", "incoming", Utils.dtmfWatcher(callp)).add()
Call(callp, ua, peerUri, "in", "incoming", null).add()
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
if (ua.account.answerMode == Api.ANSWERMODE_MANUAL) {
@ -827,47 +904,6 @@ class BaresipService: Service() {
if (am.mode != MODE_IN_COMMUNICATION)
am.mode = MODE_IN_COMMUNICATION
call!!.status = "connected"
if (recorderSessionId != 0) {
if (Utils.isAecSupported() && !webrtcAec) {
aec = AcousticEchoCanceler.create(recorderSessionId)
if (aec != null) {
if (!aec!!.getEnabled()) {
aec!!.setEnabled(true)
if (aec!!.getEnabled())
Log.d(TAG, "AEC is enabled")
else
Log.w(TAG, "Failed to enable AEC")
}
} else
Log.w(
TAG, "Failed to create AEC for session " +
"$recorderSessionId"
)
}
if (agcAvailable) {
agc = AutomaticGainControl.create(recorderSessionId)
if (agc != null) {
if (!agc!!.getEnabled()) {
agc!!.setEnabled(true)
if (agc!!.getEnabled())
Log.d(TAG, "AGC is enabled")
}
} else
Log.w(TAG, "Failed to create AGC")
}
if (nsAvailable) {
ns = NoiseSuppressor.create(recorderSessionId)
if (ns != null) {
if (!ns!!.getEnabled()) {
ns!!.setEnabled(true)
if (ns!!.getEnabled())
Log.d(TAG, "ns is enabled")
}
} else
Log.w(TAG, "Failed to create NS")
}
recorderSessionId = 0
}
call.onhold = false
if (ua.account.callHistory)
call.startTime = GregorianCalendar()
@ -951,8 +987,11 @@ class BaresipService: Service() {
stopRinging()
stopMediaPlayer()
aec?.release()
aec = null
agc?.release()
agc = null
ns?.release()
ns = null
val newCall = call.newCall
if (newCall != null) {
newCall.onHoldCall = null
@ -1237,14 +1276,14 @@ class BaresipService: Service() {
2-> R.id.status2
else -> R.id.status3
}
if (i < uas.size) {
notificationLayout.setImageViewResource(resId, uas[i].status)
if (i < uas.value.size) {
notificationLayout.setImageViewResource(resId, uas.value[i].status)
notificationLayout.setViewVisibility(resId, View.VISIBLE)
} else {
notificationLayout.setViewVisibility(resId, View.INVISIBLE)
}
}
if (uas.size > 4)
if (uas.value.size > 4)
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
else
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
@ -1467,7 +1506,7 @@ class BaresipService: Service() {
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) {
val props = cm.getLinkProperties(n) ?: continue
for (la in props.linkAddresses)
if (la.scope == android.system.OsConstants.RT_SCOPE_UNIVERSE &&
if (la.scope == OsConstants.RT_SCOPE_UNIVERSE &&
props.interfaceName != null && la.address.hostAddress != null &&
afMatch(la.address.hostAddress!!))
addresses[la.address.hostAddress!!] = props.interfaceName!!
@ -1544,9 +1583,10 @@ class BaresipService: Service() {
stopRinging()
stopMediaPlayer()
abandonAudioFocus(applicationContext)
uas.clear()
uas.value = emptyList()
uasStatus.value = emptyMap()
callHistory.clear()
messages.clear()
messages = emptyList()
if (this::nm.isInitialized)
nm.cancelAll()
if (this::partialWakeLock.isInitialized && partialWakeLock.isHeld)
@ -1590,17 +1630,20 @@ class BaresipService: Service() {
var isRecOn = false
var toneCountry = "us"
val uas = ArrayList<UserAgent>()
val uas = mutableStateOf(emptyList<UserAgent>())
val uasStatus = mutableStateOf(emptyMap<String, Int>())
var contacts by mutableStateOf(emptyList<Contact>())
val darkTheme = mutableStateOf(false)
var messages by mutableStateOf(emptyList<Message>())
val calls = ArrayList<Call>()
var callHistory = ArrayList<CallHistoryNew>()
var messages = ArrayList<Message>()
val messageUpdate = MutableLiveData<Long>()
val contactUpdate = MutableLiveData<Long>()
val registrationUpdate = MutableLiveData<Long>()
val baresipContacts = ArrayList<Contact.BaresipContact>()
val androidContacts = ArrayList<Contact.AndroidContact>()
val contacts = ArrayList<Contact>()
val contactNames = ArrayList<String>()
val baresipContacts = mutableStateOf(emptyList<Contact.BaresipContact>())
val androidContacts = mutableStateOf(emptyList<Contact.AndroidContact>())
val contactNames = mutableStateOf(emptyList<String>())
var contactsMode = "baresip"
val chatTexts: MutableMap<String, String> = mutableMapOf()
val activities = mutableListOf<String>()
@ -1611,11 +1654,12 @@ class BaresipService: Service() {
// <aor, password> of those accounts that have auth username without auth password
val aorPasswords = mutableMapOf<String, String>()
var audioFocusRequest: AudioFocusRequestCompat? = null
var agcAvailable = AutomaticGainControl.isAvailable()
private val nsAvailable = NoiseSuppressor.isAvailable()
var aecAvailable = false
private var aec: AcousticEchoCanceler? = null
private var ns: NoiseSuppressor? = null
var agcAvailable = false
private var agc: AutomaticGainControl? = null
private val nsAvailable = NoiseSuppressor.isAvailable()
private var ns: NoiseSuppressor? = null
var webrtcAec = false
private var btAdapter: BluetoothAdapter? = null
private var recorderSessionId = 0
@ -1626,7 +1670,7 @@ class BaresipService: Service() {
Log.d(TAG, "Already focused")
return true
}
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
val attributes = AudioAttributesCompat.Builder()
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
@ -1647,7 +1691,7 @@ class BaresipService: Service() {
}
fun abandonAudioFocus(ctx: Context) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
if (audioFocusRequest != null) {
Log.d(TAG, "Abandoning audio focus")
if (AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) ==
@ -1683,7 +1727,7 @@ class BaresipService: Service() {
}
private fun startBluetoothSco(ctx: Context, delay: Long, count: Int) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
if (isBluetoothScoOn(am)) {
Log.d(TAG, "Bluetooth SCO is already on")
return
@ -1705,7 +1749,7 @@ class BaresipService: Service() {
private fun stopBluetoothSco(ctx: Context) {
Log.d(TAG, "Stopping Bluetooth SCO")
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
if (!isBluetoothScoOn(am)) {
Log.d(TAG, "Bluetooth SCO is already off")
return
@ -1719,7 +1763,5 @@ class BaresipService: Service() {
am.isBluetoothScoOn = false
}, 100)
}
}
}

View File

@ -1,24 +1,67 @@
package com.tutpro.baresip
import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.format.DateUtils
import android.view.MenuItem
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import com.tutpro.baresip.databinding.ActivityCallDetailsBinding
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
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.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tutpro.baresip.CallsActivity.Companion.uaHistory
import com.tutpro.baresip.CustomElements.verticalScrollbar
import java.io.File
import java.io.IOException
import java.text.DateFormat
import java.util.GregorianCalendar
class CallDetailsActivity : AppCompatActivity() {
class CallDetailsActivity : ComponentActivity() {
private lateinit var binding: ActivityCallDetailsBinding
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
@ -33,38 +76,282 @@ class CallDetailsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_call_details)
enableEdgeToEdge()
binding = ActivityCallDetailsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.CallDetailsView.updatePadding(top = systemBars.top + 56)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
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")
val headerView = binding.peer
val headerText = "${getString(R.string.peer)} $peer"
headerView.text = headerText
val listView = binding.calls
listView.adapter = CallDetailsAdapter(this, CallsActivity.uaHistory[position].details,
decPlayer, encPlayer)
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
CallDetailsScreen(this, getString(R.string.call_details)) { goBack() }
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@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()
.background(LocalCustomColors.current.background)
.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 = getString(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 = getString(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 = getString(R.string.time),
color = LocalCustomColors.current.itemText,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.weight(1f))
Text(text = getString(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(ctx, detail)
Spacer(modifier = Modifier.weight(1f))
Duration(ctx, detail, durationText)
}
}
}
}
@Composable
fun startTime(ctx: Context, 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)
ctx.getString(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)
ctx.getString(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 recording = detail.recording
if (recording[0] != "") {
Text(
text = durationText,
color = LocalCustomColors.current.accent,
modifier = Modifier.padding(end = 12.dp)
.clickable(onClick = {
val builder = MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
builder.setTitle("Playing recording ...")
.setMessage("")
.setCancelable(true)
val dialog = builder.create()
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")
dialog.show()
}
setOnCompletionListener {
Log.d(TAG, "Stopping encPlayer")
it.stop()
dialog.cancel()
}
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()
}
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()

View File

@ -1,166 +0,0 @@
package com.tutpro.baresip
import android.annotation.SuppressLint
import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import java.io.File
import java.io.IOException
import java.text.DateFormat
import java.util.*
class CallDetailsAdapter(private val ctx: Context, private val rows: ArrayList<CallRow.Details>,
private val decPlayer: MediaPlayer, private val encPlayer: MediaPlayer) :
ArrayAdapter<CallRow.Details>(ctx, R.layout.call_detail_row, rows) {
private val layoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private class ViewHolder(view: View?) {
val directionView: ImageView = view?.findViewById(R.id.direction)!!
val timeView: TextView = view?.findViewById(R.id.time)!!
val durationView: TextView = view?.findViewById(R.id.duration)!!
}
@SuppressLint("SimpleDateFormat")
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
if (view == null) {
rowView = layoutInflater.inflate(R.layout.call_detail_row, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val direction = rows[position].direction
viewHolder.directionView.setImageResource(direction)
val startTime = rows[position].startTime
val stopTime = rows[position].stopTime
val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
ctx.getString(R.string.today) + " " + fmt.format(stopTime.time)
} else {
val fmt = DateFormat.getDateTimeInstance()
fmt.format(stopTime.time)
}
if (startTime == GregorianCalendar(0, 0, 0)) {
viewHolder.timeView.text = stopText
viewHolder.durationView.text = "?"
} else {
if (startTime == null) {
viewHolder.timeView.text = stopText
viewHolder.durationView.text = ""
} else {
val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
ctx.getString(R.string.today) + " " + fmt.format(startTime.time)
} else {
val fmt = DateFormat.getDateTimeInstance()
fmt.format(startTime.time)
}
viewHolder.timeView.text = startText
val duration = (stopTime.time.time - startTime.time.time) / 1000
viewHolder.durationView.text = DateUtils.formatElapsedTime(duration)
val recording = rows[position].recording
if (recording[0] != "") {
viewHolder.durationView.setTextColor(ContextCompat.getColor(context, R.color.colorAccent))
viewHolder.durationView.setOnClickListener {
if (!decPlayer.isPlaying && !encPlayer.isPlaying) {
decPlayer.reset()
encPlayer.reset()
playRecording(ctx, viewHolder.durationView, recording)
} else if (decPlayer.isPlaying && encPlayer.isPlaying) {
decPlayer.stop()
encPlayer.stop()
viewHolder.durationView.setTextColor(ContextCompat.getColor(ctx, R.color.colorItemText))
}
}
}
}
}
return rowView
}
private fun playRecording(ctx: Context, textView: TextView, recording: Array<String>) {
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()
textView.setTextColor(ContextCompat.getColor(ctx, R.color.colorAccent))
Log.d(TAG, "Started players")
}
setOnCompletionListener {
Log.d(TAG, "Stopping encPlayer")
it.stop()
textView.setTextColor(ContextCompat.getColor(ctx, R.color.colorItemText))
}
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()
}
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")
}
}
return
}
}

View File

@ -1,85 +0,0 @@
package com.tutpro.baresip
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
class CallListAdapter(private val ctx: Context, private val account: Account,
private val rows: ArrayList<CallRow>) :
ArrayAdapter<CallRow>(ctx, R.layout.call_row, rows) {
private val layoutInflater = LayoutInflater.from(context)
private class ViewHolder(view: View?) {
val textAvatarView: TextView = view?.findViewById(R.id.TextAvatar)!!
val imageAvatarView: ImageView = view?.findViewById(R.id.ImageAvatar)!!
val directionsView: LinearLayout = view?.findViewById(R.id.directions)!!
val etcView: TextView = view?.findViewById(R.id.etc)!!
val peerURIView: TextView = view?.findViewById(R.id.peer_uri)!!
val timeView: TextView = view?.findViewById(R.id.time)!!
}
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
if (view == null) {
rowView = layoutInflater.inflate(R.layout.call_row, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val callRow = rows[position]
Utils.setAvatar(ctx, viewHolder.imageAvatarView, viewHolder.textAvatarView, callRow.peerUri)
viewHolder.directionsView.removeAllViews()
var count = 1
for (d in callRow.details) {
if (d.recording[0] != "")
viewHolder.timeView.setTextColor(ContextCompat.getColor(context, R.color.colorAccent))
if (count > 3) {
viewHolder.etcView.text = "..."
continue
}
val dirView = ImageView(ctx)
val params = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
dirView.layoutParams = params
dirView.setPadding(0, 5, 0, 0)
dirView.setImageResource(d.direction)
viewHolder.directionsView.addView(dirView)
count++
}
if (count <= 3)
viewHolder.etcView.text = ""
viewHolder.peerURIView.text = Utils.friendlyUri(ctx, callRow.peerUri, account)
viewHolder.timeView.text = Utils.relativeTime(ctx, callRow.stopTime)
viewHolder.timeView.setOnClickListener {
val i = Intent(ctx, CallDetailsActivity::class.java)
val b = Bundle()
b.putString("aor", account.aor)
b.putString("peer", viewHolder.peerURIView.text!!.toString())
b.putInt("position", position)
i.putExtras(b)
ctx.startActivity(i)
}
return rowView
}
}

View File

@ -1,31 +1,75 @@
package com.tutpro.baresip
import android.app.Activity
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.graphics.BitmapFactory
import android.os.Bundle
import android.os.SystemClock
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.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.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.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tutpro.baresip.databinding.ActivityCallsBinding
import com.tutpro.baresip.Contact.BaresipContact
import com.tutpro.baresip.CustomElements.ImageAvatar
import com.tutpro.baresip.CustomElements.TextAvatar
import com.tutpro.baresip.CustomElements.verticalScrollbar
class CallsActivity : AppCompatActivity() {
class CallsActivity : ComponentActivity() {
private lateinit var binding: ActivityCallsBinding
private lateinit var account: Account
private lateinit var clAdapter: CallListAdapter
private var aor = ""
private var lastClick: Long = 0
@ -40,153 +84,402 @@ class CallsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityCallsBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.CallsView.updatePadding(top = systemBars.top + 56)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
val title = String.format(getString(R.string.call_history))
aor = intent.getStringExtra("aor")!!
Utils.addActivity("calls,$aor")
val ua = UserAgent.ofAor(aor)!!
account = ua.account
val headerView = binding.account
val headerText = getString(R.string.account) + " " +
if (account.nickName != "")
account.nickName
else
aor.split(":")[1]
headerView.text = headerText
val listView = binding.calls
aorGenerateHistory(aor)
clAdapter = CallListAdapter(this, account, uaHistory)
listView.adapter = clAdapter
listView.isLongClickable = true
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ ->
val peerUri = uaHistory[pos].peerUri
val peerName = Utils.friendlyUri(this, peerUri, account)
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
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
if (which == DialogInterface.BUTTON_NEGATIVE)
i.putExtra("action", "call")
else
i.putExtra("action", "message")
i.putExtra("uap", ua.uap)
i.putExtra("peer", peerUri)
startActivity(i)
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime()
with(MaterialAlertDialogBuilder(this@CallsActivity, R.style.AlertDialogTheme)) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.calls_call_message_question),
peerName
)
)
setNeutralButton(getString(R.string.cancel), dialogClickListener)
setNegativeButton(getString(R.string.call), dialogClickListener)
setPositiveButton(getString(R.string.send_message), dialogClickListener)
show()
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
CallsScreen(this, title) { goBack() }
}
}
}
val contactRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
clAdapter.notifyDataSetChanged()
account.missedCalls = false
invalidateOptionsMenu()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@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 = String.format(getString(R.string.delete))
val disable = String.format(getString(R.string.disable_history))
val enable = String.format(getString(R.string.enable_history))
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 ->
with(
MaterialAlertDialogBuilder(
this@CallsActivity,
R.style.AlertDialogTheme
)
) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.delete_history_alert),
aor.substringAfter(":")
)
)
setPositiveButton(delete) { dialog, _ ->
CallHistoryNew.clear(aor)
CallHistoryNew.save()
aorGenerateHistory(aor)
//clAdapter.notifyDataSetChanged()
dialog.dismiss()
}
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
show()
}
disable, enable -> {
account.callHistory = !account.callHistory
AccountsActivity.saveAccounts()
}
}
}
)
}
)
}
@Composable
fun CallsContent(ctx: Context, contentPadding: PaddingValues) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(LocalCustomColors.current.background)
.padding(contentPadding)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Account(account)
Calls(ctx, account)
}
}
@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),
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = LocalCustomColors.current.itemText,
textAlign = TextAlign.Center
)
}
@Composable
fun Calls(ctx: Context, account: Account) {
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 = {
callMessageClick(ctx, callRow)
},
onLongClick = {
callMessageLongClick(ctx, callRow)
}
)
) {
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)
}
}
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)
})
)
}
}
}
}
}
listView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, pos, _ ->
val peerUri = uaHistory[pos].peerUri
val peerName = Utils.friendlyUri(this, peerUri, account)
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
private fun callMessageClick(ctx: Context, callRow: CallRow) {
val peerUri = callRow.peerUri
val peerName = Utils.friendlyUri(ctx, peerUri, account)
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
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
if (which == DialogInterface.BUTTON_NEGATIVE)
i.putExtra("action", "call")
else
i.putExtra("action", "message")
i.putExtra("uap", UserAgent.ofAor(aor)!!.uap)
i.putExtra("peer", peerUri)
startActivity(i)
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime()
with(
MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
) {
setTitle(R.string.confirmation)
setMessage(
String.format(ctx.getString(R.string.contact_action_question), peerName)
)
setNeutralButton(
ctx.getText(R.string.cancel),
dialogClickListener
)
setNegativeButton(
ctx.getText(R.string.call),
dialogClickListener
)
setPositiveButton(
ctx.getText(R.string.send_message),
dialogClickListener
)
show()
}
}
}
private fun callMessageLongClick(ctx: Context, callRow: CallRow) {
val peerUri = callRow.peerUri
val peerName = Utils.friendlyUri(ctx, peerUri, account)
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_NEGATIVE -> {
val i = Intent(this, ContactActivity::class.java)
val i = Intent(ctx, BaresipContactActivity::class.java)
val b = Bundle()
b.putBoolean("new", true)
b.putString("uri", peerUri)
i.putExtras(b)
contactRequest.launch(i)
ctx.startActivity(i)
}
DialogInterface.BUTTON_POSITIVE -> {
removeUaHistoryAt(pos)
removeFromHistory(callRow)
CallHistoryNew.save()
clAdapter.notifyDataSetChanged()
/// clAdapter.notifyDataSetChanged()
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
val callText: String = if (uaHistory[pos].details.size > 1)
getString(R.string.calls_calls)
else
getString(R.string.calls_call)
val builder = MaterialAlertDialogBuilder(this@CallsActivity, R.style.AlertDialogTheme)
if (!Contact.nameExists(peerName, false))
with(builder) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.calls_add_delete_question),
peerName, callText
)
val callText: String = if (callRow.details.size > 1)
getString(R.string.calls_calls)
else
getString(R.string.calls_call)
val builder = MaterialAlertDialogBuilder(
this@CallsActivity,
R.style.AlertDialogTheme
)
if (!Contact.nameExists(peerName, BaresipService.contacts, false))
with(builder) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.calls_add_delete_question),
peerName, callText
)
setNeutralButton(getString(R.string.cancel), dialogClickListener)
setPositiveButton(getString(R.string.delete), dialogClickListener)
setNegativeButton(getString(R.string.add_contact), dialogClickListener)
show()
}
else
with(builder) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.calls_delete_question),
peerName, callText
)
)
setNeutralButton(
getString(R.string.cancel),
dialogClickListener
)
setPositiveButton(
getString(R.string.delete),
dialogClickListener
)
setNegativeButton(
getString(R.string.add_contact),
dialogClickListener
)
show()
}
else
with(builder) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.calls_delete_question),
peerName, callText
)
setNeutralButton(getString(R.string.cancel), dialogClickListener)
setPositiveButton(getString(R.string.delete), dialogClickListener)
show()
}
true
}
ua.account.missedCalls = false
invalidateOptionsMenu()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
)
setNeutralButton(
getString(R.string.cancel),
dialogClickListener
)
setPositiveButton(
getString(R.string.delete),
dialogClickListener
)
show()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -205,7 +498,7 @@ class CallsActivity : AppCompatActivity() {
CallHistoryNew.clear(aor)
CallHistoryNew.save()
aorGenerateHistory(aor)
clAdapter.notifyDataSetChanged()
// clAdapter.notifyDataSetChanged()
dialog.dismiss()
}
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
@ -243,7 +536,7 @@ class CallsActivity : AppCompatActivity() {
}
private fun returnResult() {
setResult(Activity.RESULT_CANCELED, Intent())
setResult(RESULT_CANCELED, Intent())
finish()
}
@ -263,7 +556,7 @@ class CallsActivity : AppCompatActivity() {
}
private fun aorGenerateHistory(aor: String) {
uaHistory.clear()
uaHistory.value = emptyList()
for (i in BaresipService.callHistory.indices.reversed()) {
val h = BaresipService.callHistory[i]
if (h.aor == aor) {
@ -286,34 +579,54 @@ class CallsActivity : AppCompatActivity() {
R.drawable.call_missed_out
}
}
if (uaHistory.isNotEmpty() && (uaHistory.last().peerUri == h.peerUri))
uaHistory.last().details.add(CallRow.Details(
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
uaHistory.add(CallRow(
h.aor, h.peerUri, direction, h.startTime, h.stopTime,
h.recording
))
addToUaHistory(
CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording)
)
}
}
}
private fun removeUaHistoryAt(i: Int) {
for (details in uaHistory[i].details) {
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(uaHistory[i].recording)
uaHistory.removeAt(i)
CallHistoryNew.deleteRecording(callRow.recording)
deleteFromUaHistory(callRow)
updateUaHistory()
}
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
}
private fun updateUaHistory() {
val updatedList: MutableList<CallRow> = mutableListOf()
for (h in uaHistory.value)
updatedList.add(h)
uaHistory.value = updatedList
}
@SuppressLint("MutableCollectionMutableState")
companion object {
var uaHistory = ArrayList<CallRow>()
val uaHistory = mutableStateOf(emptyList<CallRow>())
}
}

View File

@ -1,45 +1,103 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
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.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ListView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.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.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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.painterResource
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.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Observer
import com.tutpro.baresip.databinding.ActivityChatBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tutpro.baresip.CustomElements.Text
import com.tutpro.baresip.CustomElements.verticalScrollbar
import kotlinx.coroutines.launch
import java.text.DateFormat
import java.util.GregorianCalendar
class ChatActivity : AppCompatActivity() {
class ChatActivity : ComponentActivity() {
private lateinit var binding: ActivityChatBinding
private lateinit var chatMessages: ArrayList<Message>
private lateinit var mlAdapter: MessageListAdapter
private lateinit var listView: ListView
private lateinit var newMessage: EditText
private lateinit var sendButton: ImageButton
private lateinit var imm: InputMethodManager
private lateinit var aor: String
private lateinit var account: Account
private lateinit var peerUri: String
private lateinit var chatPeer: 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 unsentMessage = ""
private var keyboardController: SoftwareKeyboardController? = null
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@ -51,28 +109,15 @@ class ChatActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.ChatView.updatePadding(top = systemBars.top + 56)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
supportActionBar?.setDisplayHomeAsUpEnabled(true)
enableEdgeToEdge()
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(Activity.RESULT_CANCELED)
returnResult(RESULT_CANCELED)
return
} else {
Utils.addActivity("chat,$aor,$peerUri,$focus")
@ -82,92 +127,474 @@ class ChatActivity : AppCompatActivity() {
if (userAgent == null) {
Log.w(TAG, "MessageActivity did not find ua of $aor")
MainActivity.activityAor = aor
returnResult(Activity.RESULT_CANCELED)
returnResult(RESULT_CANCELED)
return
} else {
ua = userAgent
}
chatPeer = Utils.friendlyUri(this, peerUri, userAgent.account, true)
imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
this@ChatActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
val chatPeer = Utils.friendlyUri(this, peerUri, userAgent.account, true)
title = String.format(getString(R.string.chat_with), chatPeer)
val headerView = binding.account
val headerText = getString(R.string.account) + " " +
if (ua.account.nickName != "")
ua.account.nickName
else
aor.split(":")[1]
headerView.text = headerText
listView = binding.messages
chatMessages = uaPeerMessages(aor, peerUri)
mlAdapter = MessageListAdapter(this, peerUri, chatPeer, chatMessages)
val title = String.format(getString(R.string.chat_with), chatPeer)
val messagesObserver = Observer<Long> {
chatMessages.clear()
chatMessages.addAll(uaPeerMessages(aor, peerUri))
mlAdapter.notifyDataSetChanged()
_chatMessages.value = listOf()
_chatMessages.value = uaPeerMessages(aor, peerUri)
}
BaresipService.messageUpdate.observe(this, messagesObserver)
listView.adapter = mlAdapter
//listView.isLongClickable = true
val footerView = View(applicationContext)
listView.addFooterView(footerView)
listView.smoothScrollToPosition(mlAdapter.count - 1)
newMessage = binding.text
newMessage.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
}
if (focus) newMessage.requestFocus()
sendButton = binding.sendButton
sendButton.setOnClickListener {
val msgText = newMessage.text.toString()
if (msgText.isNotEmpty()) {
imm.hideSoftInputFromWindow(newMessage.windowToken, 0)
val time = System.currentTimeMillis()
val msg = Message(aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true)
msg.add()
var msgUri = ""
chatMessages.add(msg)
if (Utils.isTelUri(peerUri))
if (ua.account.telProvider == "") {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.no_telephony_provider),
Utils.plainAor(aor)))
} 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(applicationContext, "${getString(R.string.message_failed)}!",
Toast.LENGTH_SHORT).show()
msg.direction = MESSAGE_UP_FAIL
msg.responseReason = getString(R.string.message_failed)
} else {
newMessage.setText("")
BaresipService.chatTexts.remove("$aor::$peerUri")
}
mlAdapter.notifyDataSetChanged()
}
}
ua.account.unreadMessages = false
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
setContent {
AppTheme {
keyboardController = LocalSoftwareKeyboardController.current
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
ChatScreen(this, title) { goBack() }
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@Composable
fun ChatScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
Scaffold(
modifier = Modifier
.fillMaxHeight()
.imePadding()
.safeDrawingPadding(),
containerColor = LocalCustomColors.current.background,
topBar = { TopAppBar(ctx, title, navigateBack) },
bottomBar = { NewMessage(ctx, peerUri) },
content = { contentPadding ->
ChatContent(ctx, contentPadding)
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBar(ctx: Context, title: String, navigateBack: () -> Unit) {
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 = {
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()
.fillMaxHeight()
.background(LocalCustomColors.current.background)
.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 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
)
.background(LocalCustomColors.current.background),
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.startsWith("sip:") &&
(Utils.uriHostPart(message.peerUri) == Utils.uriHostPart(message.aor)))
Utils.uriUserPart(message.peerUri)
else
chatPeer
}
else
ctx.getString(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 = "${ctx.getString(R.string.today)} $info"
if (message.direction == MESSAGE_UP_FAIL) {
info = if (message.responseCode != 0)
"$info - ${ctx.getString(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
else
"$info - ${ctx.getString(R.string.sending_failed)}"
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(end = 12.dp)
) {
Button(
onClick = {
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
val i = Intent(ctx, BaresipContactActivity::class.java)
val b = Bundle()
b.putBoolean("new", true)
b.putString("uri", peerUri)
i.putExtras(b)
ctx.startActivity(i)
}
DialogInterface.BUTTON_NEGATIVE -> {
message.delete()
_chatMessages.value = uaPeerMessages(aor, peerUri)
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
val builder = MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
if (chatPeer == peerUri)
with(builder) {
setTitle(R.string.confirmation)
setMessage(
String.format(
ctx.getString(R.string.long_message_question),
peerUri
)
)
setNeutralButton(
ctx.getString(R.string.cancel),
dialogClickListener
)
setNegativeButton(
ctx.getString(R.string.delete),
dialogClickListener
)
setPositiveButton(
ctx.getString(R.string.add_contact),
dialogClickListener
)
show()
}
else
with(builder) {
setTitle(R.string.confirmation)
setMessage(ctx.getText(R.string.short_message_question))
setNeutralButton(
ctx.getString(R.string.cancel),
dialogClickListener
)
setNegativeButton(
ctx.getString(R.string.delete),
dialogClickListener
)
show()
}
},
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)
LocalCustomColors.current.secondaryLight
else
LocalCustomColors.current.primaryLight),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Column {
Row {
Text(
peer, color = LocalCustomColors.current.dark,
fontSize = 12.sp
)
Spacer(modifier = Modifier.weight(1f))
Text(
info, color = LocalCustomColors.current.dark,
fontSize = 12.sp
)
}
Row {
SelectionContainer {
Text(
text = message.message,
color = LocalCustomColors.current.dark,
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 by remember { mutableStateOf("") }
// var textFieldState = rememberTextFieldState()
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,
placeholder = { Text(text = getString(R.string.new_message)) },
onValueChange = { newMessage = it },
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
.verticalScroll(rememberScrollState())
.combinedClickable(
onClick = {
val msgText = newMessage
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 == "") {
Utils.alertView(
ctx, getString(R.string.notice),
String.format(
getString(R.string.no_telephony_provider),
Utils.plainAor(aor)
)
)
} 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 = ""
unsentMessage = ""
keyboardController?.hide()
BaresipService.chatTexts.remove("$aor::$peerUri")
}
}
},
onLongClick = {
val clipboardManager =
ctx.getSystemService(Context.CLIPBOARD_SERVICE) as
android.content.ClipboardManager
val clipData = clipboardManager.primaryClip
if (clipData != null && clipData.itemCount > 0) {
val pastedText = clipData.getItemAt(0).text.toString()
newMessage = pastedText
} else {
Toast.makeText(ctx, "Nothing to paste", Toast.LENGTH_SHORT).show()
}
},
),
singleLine = false,
trailingIcon = {
if (newMessage.isNotEmpty()) {
Icon(
Icons.Outlined.Clear,
contentDescription = "Clear",
modifier = Modifier.clickable { newMessage = "" }
)
} },
label = { Text(text = getString(R.string.new_message), fontSize = 18.sp) },
textStyle = TextStyle(fontSize = 18.sp),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
keyboardType = KeyboardType.Text,
autoCorrectEnabled = true
)
)
Image(
painter = painterResource(id = R.drawable.send),
contentDescription = "Send",
contentScale = ContentScale.Crop,
modifier = Modifier.size(36.dp)
.clickable {
val msgText = newMessage
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 == "") {
Utils.alertView(
ctx, getString(R.string.notice),
String.format(
getString(R.string.no_telephony_provider),
Utils.plainAor(aor)
)
)
} 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 = ""
unsentMessage = ""
keyboardController?.hide()
BaresipService.chatTexts.remove("$aor::$peerUri")
}
}
}
)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -179,61 +606,24 @@ class ChatActivity : AppCompatActivity() {
super.onResume()
val chatText = BaresipService.chatTexts["$aor::$peerUri"]
if (chatText != null) {
Log.d(TAG, "Restoring newMessage ${newMessage.text} for $aor::$peerUri")
newMessage.setText(chatText)
newMessage.requestFocus()
Log.d(TAG, "Restoring newMessage $chatText for $aor::$peerUri")
unsentMessage = chatText
//newMessage.requestFocus()
BaresipService.chatTexts.remove("$aor::$peerUri")
}
chatMessages.clear()
chatMessages.addAll(uaPeerMessages(aor, peerUri))
mlAdapter.notifyDataSetChanged()
_chatMessages.value = uaPeerMessages(aor, peerUri)
}
override fun onPause() {
super.onPause()
if (newMessage.text.toString() != "") {
Log.d(TAG, "Saving newMessage ${newMessage.text} for $aor::$peerUri")
BaresipService.chatTexts["$aor::$peerUri"] = newMessage.text.toString()
if (unsentMessage != "") {
Log.d(TAG, "Saving newMessage $unsentMessage for $aor::$peerUri")
BaresipService.chatTexts["$aor::$peerUri"] = unsentMessage
}
MainActivity.activityAor = aor
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if ((BaresipService.activities.indexOf("chat,$aor,$peerUri,false") == -1) &&
(BaresipService.activities.indexOf("chat,$aor,$peerUri,true") == -1))
return true
when (item.itemId) {
R.id.callIcon -> {
if (SystemClock.elapsedRealtime() - lastCall > 1000) {
lastCall = SystemClock.elapsedRealtime()
val intent = Intent(this, 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)
finish()
return true
}
}
android.R.id.home -> {
goBack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun goBack() {
var save = false
for (m in chatMessages) {
if (m.new) {
@ -242,13 +632,10 @@ class ChatActivity : AppCompatActivity() {
}
}
if (save) Message.save()
imm.hideSoftInputFromWindow(newMessage.windowToken, 0)
keyboardController?.hide()
BaresipService.activities.remove("chat,$aor,$peerUri,false")
BaresipService.activities.remove("chat,$aor,$peerUri,true")
returnResult(Activity.RESULT_OK)
returnResult(RESULT_OK)
}
private fun returnResult(code: Int) {
@ -256,9 +643,9 @@ class ChatActivity : AppCompatActivity() {
finish()
}
private fun uaPeerMessages(aor: String, peerUri: String): ArrayList<Message> {
val res = ArrayList<Message>()
for (m in Message.messages())
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
}

View File

@ -1,83 +0,0 @@
package com.tutpro.baresip
import android.content.Context
import android.graphics.Typeface
import android.text.format.DateUtils.isToday
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import java.text.DateFormat
import java.util.*
class ChatListAdapter(private val ctx: Context, private val account: Account, private val rows: ArrayList<Message>) :
ArrayAdapter<Message>(ctx, R.layout.message, rows) {
private val layoutInflater = LayoutInflater.from(context)
private class ViewHolder(view: View?) {
val textAvatarView: TextView = view?.findViewById(R.id.TextAvatar)!!
val imageAvatarView: ImageView = view?.findViewById(R.id.ImageAvatar)!!
val layoutView: LinearLayout = view?.findViewById(R.id.chat)!!
val peerView: TextView = view?.findViewById(R.id.peer)!!
val infoView: TextView = view?.findViewById(R.id.info)!!
val textView: TextView = view?.findViewById(R.id.text)!!
}
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
if (view == null) {
rowView = layoutInflater.inflate(R.layout.chat_row, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val message = rows[position]
viewHolder.peerView.text = Utils.friendlyUri(ctx, message.peerUri, account)
Utils.setAvatar(ctx, viewHolder.imageAvatarView, viewHolder.textAvatarView,
if (Contact.contactName(message.peerUri) == message.peerUri)
Utils.e164Uri(message.peerUri, account.countryCode)
else
message.peerUri)
if (message.direction == MESSAGE_DOWN)
viewHolder.layoutView.setBackgroundResource(R.drawable.message_in_bg)
else
viewHolder.layoutView.setBackgroundResource(R.drawable.message_out_bg)
val cal = GregorianCalendar()
cal.timeInMillis = message.timeStamp
val fmt: DateFormat = if (isToday(message.timeStamp))
DateFormat.getTimeInstance(DateFormat.SHORT)
else
DateFormat.getDateInstance(DateFormat.SHORT)
viewHolder.infoView.text = fmt.format(cal.time)
if (message.direction == MESSAGE_UP_FAIL) {
val info: String = if (message.responseCode != 0)
"${viewHolder.infoView.text} - ${ctx.getString(R.string.message_failed)}: " +
"${message.responseCode} ${message.responseReason}"
else
"${viewHolder.infoView.text} - ${ctx.getString(R.string.sending_failed)}"
viewHolder.infoView.text = info
viewHolder.infoView.setTextColor(ContextCompat.getColor(ctx, R.color.colorAccent))
}
viewHolder.textView.text = message.message
if (message.new)
viewHolder.textView.setTypeface(null, Typeface.BOLD)
return rowView
}
}

View File

@ -1,38 +1,98 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.graphics.BitmapFactory
import android.os.Bundle
import android.text.format.DateUtils.isToday
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
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.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import androidx.compose.animation.animateContentSize
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.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.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.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.SmallFloatingActionButton
import androidx.compose.material3.Surface
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.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tutpro.baresip.databinding.ActivityChatsBinding
import com.tutpro.baresip.BaresipService.Companion.contactNames
import com.tutpro.baresip.CustomElements.ImageAvatar
import com.tutpro.baresip.CustomElements.Text
import com.tutpro.baresip.CustomElements.TextAvatar
import com.tutpro.baresip.CustomElements.verticalScrollbar
import java.text.DateFormat
import java.util.GregorianCalendar
class ChatsActivity: AppCompatActivity() {
class ChatsActivity: ComponentActivity() {
private lateinit var binding: ActivityChatsBinding
private lateinit var uaMessages: ArrayList<Message>
private lateinit var listView: ListView
private lateinit var clAdapter: ChatListAdapter
internal lateinit var peerUri: AutoCompleteTextView
private lateinit var plusButton: ImageButton
internal lateinit var aor: String
internal lateinit var account: Account
private lateinit var chatRequest: ActivityResultLauncher<Intent>
private var scrollPosition = -1
private var _uaMessages = mutableStateOf<List<Message>>(emptyList())
private var uaMessages : List<Message> by _uaMessages
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@ -44,172 +104,536 @@ class ChatsActivity: AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityChatsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.ChatsView.updatePadding(top = systemBars.top + 56)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
listView = binding.chats
plusButton = binding.plusButton
enableEdgeToEdge()
aor = intent.extras!!.getString("aor")!!
Utils.addActivity("chats,$aor")
account = UserAgent.ofAor(aor)!!.account
val headerView = binding.account
val headerText = getString(R.string.account) + " " +
if (account.nickName != "")
account.nickName
else
aor.split(":")[1]
headerView.text = headerText
val title = getString(R.string.chats)
uaMessages = uaMessages(aor)
clAdapter = ChatListAdapter(this, account, uaMessages)
listView.adapter = clAdapter
listView.isLongClickable = true
_uaMessages.value = uaMessages(aor)
chatRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
clAdapter.clear()
uaMessages = uaMessages(aor)
clAdapter = ChatListAdapter(this, account, uaMessages)
listView.adapter = clAdapter
}
if (it.resultCode == RESULT_OK)
_uaMessages.value = uaMessages(aor)
}
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ ->
scrollPosition = pos
val i = Intent(this, ChatActivity::class.java)
val b = Bundle()
b.putString("aor", aor)
b.putString("peer", uaMessages[pos].peerUri)
i.putExtras(b)
chatRequest.launch(i)
}
listView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, pos, _ ->
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
val i = Intent(this, ContactActivity::class.java)
val b = Bundle()
b.putBoolean("new", true)
b.putString("uri", uaMessages[pos].peerUri)
i.putExtras(b)
startActivity(i)
}
DialogInterface.BUTTON_NEGATIVE -> {
val peerUri = uaMessages[pos].peerUri
val messages = ArrayList<Message>()
for (m in Message.messages())
if ((m.aor != aor) || (m.peerUri != peerUri))
messages.add(m)
else
clAdapter.remove(m)
clAdapter.notifyDataSetChanged()
BaresipService.messages = messages
Message.save()
uaMessages = uaMessages(aor)
}
DialogInterface.BUTTON_NEUTRAL -> {
}
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
ChatsScreen(this, title) { goBack() }
}
}
val builder = MaterialAlertDialogBuilder(this@ChatsActivity, R.style.AlertDialogTheme)
val peer = Utils.friendlyUri(this@ChatsActivity, uaMessages[pos].peerUri, account)
if (!Contact.nameExists(peer, false))
with (builder) {
setTitle(R.string.confirmation)
setMessage(String.format(getString(R.string.long_chat_question),
peer))
setNeutralButton(getText(R.string.cancel), dialogClickListener)
setNegativeButton(getText(R.string.delete), dialogClickListener)
setPositiveButton(getText(R.string.add_contact), dialogClickListener)
show()
}
else
with (builder) {
setTitle(R.string.confirmation)
setMessage(String.format(getString(R.string.short_chat_question), peer))
setNeutralButton(getText(R.string.cancel), dialogClickListener)
setNegativeButton(getText(R.string.delete), dialogClickListener)
show()
}
true
}
peerUri = binding.peer
peerUri.threshold = 2
peerUri.setAdapter(ArrayAdapter(this, android.R.layout.select_dialog_item,
Contact.contactNames()))
plusButton.setOnClickListener {
makeChat(true)
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
private fun makeChat(lookForContact: Boolean = true) {
var uriText = peerUri.text.toString().trim()
if (uriText.isNotEmpty()) {
@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 = String.format(getString(R.string.delete))
val disable = String.format(getString(R.string.disable_history))
val enable = String.format(getString(R.string.enable_history))
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, if (account.callHistory) disable else enable),
onItemClick = { selectedItem ->
expanded = false
when (selectedItem) {
delete ->
with(
MaterialAlertDialogBuilder(
this@ChatsActivity,
R.style.AlertDialogTheme
)
) {
setTitle(R.string.confirmation)
setMessage(String.format(getString(R.string.delete_chats_alert),
aor.substringAfter(":")))
setPositiveButton(delete) { dialog, _ ->
Message.clearMessagesOfAor(aor)
Message.save()
_uaMessages.value = listOf()
account.unreadMessages = false
dialog.dismiss()
}
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
show()
}
}
}
)
}
)
}
@Composable
fun ChatsContent(ctx: Context, contentPadding: PaddingValues) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(LocalCustomColors.current.background)
.padding(contentPadding),
verticalArrangement = Arrangement.Bottom
) {
Account(account)
Spacer(modifier = Modifier.weight(1f))
Chats(ctx, account)
}
}
@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 Chats(ctx: Context, account: Account) {
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),
reverseLayout = true,
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
items(items = uaMessages, key = { message -> message.timeStamp }) { message ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.combinedClickable(
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 dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
val i =
Intent(ctx, BaresipContactActivity::class.java)
val b = Bundle()
b.putBoolean("new", true)
b.putString("uri", message.peerUri)
i.putExtras(b)
startActivity(i)
}
DialogInterface.BUTTON_NEGATIVE -> {
Message.deleteAorPeerMessages(aor, message.peerUri)
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
val builder = MaterialAlertDialogBuilder(
this@ChatsActivity,
R.style.AlertDialogTheme
)
val peer = Utils.friendlyUri(ctx, message.peerUri, account)
if (!Contact.nameExists(peer, BaresipService.contacts, false))
with(builder) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.long_chat_question),
peer
)
)
setNeutralButton(
getText(R.string.cancel),
dialogClickListener
)
setNegativeButton(
getText(R.string.delete),
dialogClickListener
)
setPositiveButton(
getText(R.string.add_contact),
dialogClickListener
)
show()
}
else
with(builder) {
setTitle(R.string.confirmation)
setMessage(
String.format(
getString(R.string.short_chat_question),
peer
)
)
setNeutralButton(
getText(R.string.cancel),
dialogClickListener
)
setNegativeButton(
getText(R.string.delete),
dialogClickListener
)
show()
}
}
)) {
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))
Button(
onClick = {
val i = Intent(this@ChatsActivity, ChatActivity::class.java)
val b = Bundle()
b.putString("aor", aor)
b.putString("peer", message.peerUri)
i.putExtras(b)
chatRequest.launch(i) },
shape = if (message.direction == MESSAGE_DOWN)
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
else
RoundedCornerShape(20.dp, 50.dp, 20.dp, 10.dp),
colors = ButtonDefaults.buttonColors(containerColor =
if (message.direction == MESSAGE_DOWN)
LocalCustomColors.current.secondaryLight
else
LocalCustomColors.current.primaryLight),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 8.dp, end = 16.dp)
) {
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 {
Text(
peer, color = LocalCustomColors.current.dark,
fontSize = 12.sp
)
Spacer(modifier = Modifier.weight(1f))
Text(
info, color = LocalCustomColors.current.dark,
fontSize = 12.sp
)
}
Row {
Text(
text = message.message, color = LocalCustomColors.current.dark,
maxLines = 1
)
}
}
}
}
}
}
}
@Composable
fun NewChatPeer() {
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()) {
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(text = getString(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 {
newPeer = ""
showSuggestions = false
}
)
},
label = {
Text(
text = getString(R.string.new_chat_peer),
fontSize = 18.sp
)
},
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
if (makeChat(newPeer, 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(peer: String, lookForContact: Boolean = true): Boolean {
var peerUri = peer.trim()
if (peerUri.isNotEmpty()) {
if (lookForContact) {
val uris = Contact.contactUris(uriText)
val uris = Contact.contactUris(peerUri)
if (uris.size == 1)
uriText = uris[0]
peerUri = uris[0]
else if (uris.size > 1) {
val builder = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
with(builder) {
setTitle("Choose Destination")
setItems(uris.toTypedArray()) { _, which ->
peerUri.setText(uris[which])
makeChat(false)
peerUri = uris[which]
makeChat(peerUri, false)
}
setNeutralButton(getString(R.string.cancel)) { _: DialogInterface, _: Int -> }
show()
}
return
return true
}
}
if (Utils.isTelNumber(uriText))
uriText = "tel:$uriText"
val uri = if (Utils.isTelUri(uriText)) {
if (Utils.isTelNumber(peerUri))
peerUri = "tel:$peerUri"
val uri = if (Utils.isTelUri(peerUri)) {
if (account.telProvider == "") {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.no_telephony_provider), account.aor))
return
return false
}
Utils.telToSip(uriText, account)
Utils.telToSip(peerUri, account)
} else {
Utils.uriComplete(uriText, aor)
Utils.uriComplete(peerUri, aor)
}
if (!Utils.checkUri(uri)) {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.invalid_sip_or_tel_uri), uri))
return false
} else {
peerUri.text.clear()
peerUri.isCursorVisible = false
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)
return true
}
}
return false
}
override fun onPause() {
@ -219,18 +643,7 @@ class ChatsActivity: AppCompatActivity() {
override fun onResume() {
super.onResume()
clAdapter.clear()
uaMessages = uaMessages(aor)
clAdapter = ChatListAdapter(this, account, uaMessages)
listView.adapter = clAdapter
if (uaMessages.isNotEmpty()) {
if (scrollPosition >= 0) {
listView.setSelection(scrollPosition)
scrollPosition = -1
} else {
listView.setSelection(0)
}
}
_uaMessages.value = uaMessages(aor)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -238,55 +651,20 @@ class ChatsActivity: AppCompatActivity() {
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.delete_chats -> {
with (MaterialAlertDialogBuilder(this@ChatsActivity, R.style.AlertDialogTheme)) {
setTitle(R.string.confirmation)
setMessage(String.format(getString(R.string.delete_chats_alert),
aor.substringAfter(":")))
setPositiveButton(getText(R.string.delete)) { dialog, _ ->
Message.clear(aor)
Message.save()
uaMessages.clear()
clAdapter.notifyDataSetChanged()
account.unreadMessages = false
dialog.dismiss()
}
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
show()
}
return true
}
android.R.id.home -> {
goBack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun goBack() {
BaresipService.activities.remove("chats,$aor")
returnResult()
}
private fun returnResult() {
setResult(Activity.RESULT_CANCELED, Intent())
setResult(RESULT_CANCELED, Intent())
finish()
}
private fun uaMessages(aor: String) : ArrayList<Message> {
val res = ArrayList<Message>()
private fun uaMessages(aor: String) : List<Message> {
val res = mutableListOf<Message>()
account.unreadMessages = false
for (m in Message.messages().reversed()) {
for (m in BaresipService.messages) {
if (m.aor != aor) continue
var found = false
for (r in res)
@ -300,30 +678,6 @@ class ChatsActivity: AppCompatActivity() {
account.unreadMessages = true
}
}
return res
}
companion object {
fun saveUaMessage(aor: String, time: Long) {
for (i in Message.messages().indices.reversed())
if ((Message.messages()[i].aor == aor) &&
(Message.messages()[i].timeStamp == time)) {
Message.messages()[i].new = false
Message.save()
return
}
}
fun deleteUaMessage(aor: String, time: Long) {
for (i in Message.messages().indices.reversed())
if ((Message.messages()[i].aor == aor) &&
(Message.messages()[i].timeStamp == time)) {
Message.messages().removeAt(i)
Message.save()
return
}
}
return res.toList()
}
}

View File

@ -1,3 +1,5 @@
package com.tutpro.baresip
data class Codec(val name: String, var enabled: Boolean)
import androidx.compose.runtime.MutableState
data class Codec(val name: String, var enabled: MutableState<Boolean>)

View File

@ -1,35 +1,64 @@
package com.tutpro.baresip
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.*
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.tutpro.baresip.databinding.ActivityCodecsBinding
import java.util.*
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.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.verticalScrollbar
class CodecsActivity : AppCompatActivity() {
class CodecsActivity : ComponentActivity() {
private lateinit var binding: ActivityCodecsBinding
private lateinit var acc: Account
private lateinit var ua: UserAgent
private lateinit var codecsAdapter: CodecsAdapter
private lateinit var allCodecs: List<String>
private lateinit var accCodecs: List<String>
private lateinit var codecs: SnapshotStateList<Codec>
private var aor = ""
private var newCodecs = ArrayList<Codec>()
private var media = ""
private var title = ""
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@ -41,21 +70,7 @@ class CodecsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityCodecsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.CodecsView.updatePadding(top = systemBars.top + 64)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
supportActionBar?.setDisplayHomeAsUpEnabled(true)
enableEdgeToEdge()
aor = intent.getStringExtra("aor")!!
media = intent.getStringExtra("media")!!
@ -65,88 +80,256 @@ class CodecsActivity : AppCompatActivity() {
ua = UserAgent.ofAor(aor)!!
acc = ua.account
val allCodecs: ArrayList<String>
val accCodecs: ArrayList<String>
val codecsTitle = binding.CodecsTitle
val codecList = binding.CodecList
itemTouchHelper.attachToRecyclerView(codecList)
if (media == "audio") {
codecsTitle.text = getString(R.string.audio_codecs)
title = getString(R.string.audio_codecs)
allCodecs = ArrayList(Api.audio_codecs().split(","))
accCodecs = acc.audioCodec
} else {
codecsTitle.text = getString(R.string.video_codecs)
title = getString(R.string.video_codecs)
allCodecs = ArrayList(Api.video_codecs().split(",").distinct())
accCodecs = acc.videoCodec
}
val currentCodecs = mutableListOf<Codec>()
for (codec in accCodecs)
newCodecs.add(Codec(codec, true))
currentCodecs.add(Codec(codec, mutableStateOf(true)))
for (codec in allCodecs)
if (codec !in accCodecs)
newCodecs.add(Codec(codec, false))
currentCodecs.add(Codec(codec, mutableStateOf(false)))
codecsAdapter = CodecsAdapter(newCodecs)
codecs = mutableStateListOf<Codec>().apply { addAll(currentCodecs) }
codecList.layoutManager = LinearLayoutManager(this)
codecList.adapter = codecsAdapter
itemTouchHelper.attachToRecyclerView(binding.CodecList)
codecsTitle.setOnClickListener {
if (media == "audio")
Utils.alertView(this, getString(R.string.audio_codecs),
getString(R.string.audio_codecs_help))
else
Utils.alertView(this, getString(R.string.video_codecs),
getString(R.string.video_codecs_help))
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
CodecsScreen { goBack() }
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
private val itemTouchHelper by lazy {
val simpleItemTouchCallback = object : SimpleCallback(
UP or DOWN or START or END, RIGHT) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CodecsScreen(navigateBack: () -> Unit) {
Scaffold(
modifier = Modifier
.fillMaxHeight()
.imePadding()
.safeDrawingPadding(),
containerColor = LocalCustomColors.current.background,
topBar = {
TopAppBar(
title = {
Text(
text = getString(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)
},
)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
codecsAdapter.moveItem(fromPosition, toPosition)
codecsAdapter.notifyItemMoved(fromPosition, toPosition)
return true
@Composable
fun CodecsContent(contentPadding: PaddingValues) {
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")
Utils.alertView(
this, getString(R.string.audio_codecs),
getString(R.string.audio_codecs_help)
)
else
Utils.alertView(
this, getString(R.string.video_codecs),
getString(R.string.video_codecs_help)
)
},
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())
}
)
@SuppressLint("NotifyDataSetChanged")
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
if (newCodecs[position].enabled) {
codecsAdapter.disableItem(position)
codecsAdapter.notifyItemRemoved(position)
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 {
codecsAdapter.enableItem(position)
codecsAdapter.notifyDataSetChanged()
Log.e(TAG, "Setting of audio codecs '$codecList' failed")
}
}
@SuppressLint("NotifyDataSetChanged")
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
recyclerView.post { codecsAdapter.notifyDataSetChanged() }
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")
}
}
}
ItemTouchHelper(simpleItemTouchCallback)
if (save)
AccountsActivity.saveAccounts()
BaresipService.activities.remove("codecs,$aor,$media")
finish()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
@ -156,65 +339,6 @@ class CodecsActivity : AppCompatActivity() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (BaresipService.activities.indexOf("codecs,$aor,$media") == -1)
return true
when (item.itemId) {
R.id.checkIcon -> {
var save = false
val codecs = ArrayList<String>()
for (codec in newCodecs)
if (codec.enabled)
codecs.add(codec.name)
val codecList = Utils.implode(codecs, ",")
if (media == "audio")
if (codecs != acc.audioCodec) {
if (Api.account_set_audio_codecs(acc.accp, codecList) == 0) {
Log.d(TAG, "New audio codecs '$codecList'")
acc.audioCodec = codecs
save = true
} else {
Log.e(TAG, "Setting of audio codecs '$codecList' failed")
}
}
if (media == "video")
if (codecs != acc.videoCodec) {
if (Api.account_set_video_codecs(acc.accp, codecList) == 0) {
Log.d(TAG, "New video codecs '$codecs'")
acc.videoCodec = codecs
save = true
} else {
Log.e(TAG, "Setting of video codecs '$codecs' failed")
}
}
if (save)
AccountsActivity.saveAccounts()
BaresipService.activities.remove("codecs,$aor,$media")
finish()
return true
}
android.R.id.home -> {
goBack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun goBack() {
BaresipService.activities.remove("codecs,$aor,$media")
finish()

View File

@ -1,59 +0,0 @@
package com.tutpro.baresip
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.util.*
class CodecsAdapter(private val codecs: ArrayList<Codec>):
RecyclerView.Adapter<CodecsAdapter.CodecViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CodecViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.codec, parent, false)
return CodecViewHolder(v)
}
override fun getItemCount(): Int {
return codecs.size
}
fun moveItem(fromPosition: Int, toPosition: Int) {
val codec: Codec = codecs[fromPosition]
codec.enabled = codecs[toPosition].enabled
codecs.removeAt(fromPosition)
codecs.add(toPosition, codec)
}
fun enableItem(position: Int) {
val codec: Codec = codecs[position]
codec.enabled = true
codecs.removeAt(position)
codecs.add(0, codec)
}
fun disableItem(position: Int) {
val codec: Codec = codecs[position]
if (codec.enabled) {
codec.enabled = false
codecs.removeAt(position)
codecs.add(codec)
}
}
override fun onBindViewHolder(holder: CodecViewHolder, position: Int) {
val codec = codecs[position]
holder.codecName.text = codec.name
holder.codecName.alpha = if (codec.enabled) 1.0f else 0.5f
holder.reorderImage.setImageResource(
if (codec.enabled) R.drawable.reorder else android.R.color.transparent
)
}
class CodecViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
var codecName: TextView = itemView.findViewById(R.id.codecName)
var reorderImage: ImageView = itemView.findViewById(R.id.reorderImage)
}
}

View File

@ -153,11 +153,10 @@ object Config {
if ("${module}.so" in previousModules)
config = "${config}module ${module}.so\n"
if ((!Utils.isAecSupported() && !File(configPath).exists()) ||
"webrtc_aecm.so" in previousModules) {
Utils.aecAgcCheck()
if (!BaresipService.aecAvailable)
config = "${config}module webrtc_aecm.so\n"
BaresipService.webrtcAec = true
}
val micGain = previousVariable("augain")
config = if (BaresipService.agcAvailable || micGain == "" || micGain == "1.0")

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,6 @@ const val DEFAULT_CHANNEL_ID = "com.tutpro.baresip.default"
const val MEDIUM_CHANNEL_ID = "com.tutpro.baresip.medium"
const val HIGH_CHANNEL_ID = "com.tutpro.baresip.high"
const val CONTACTS_PERMISSION_REQUEST_CODE = 1
const val STATUS_NOTIFICATION_ID = 101
const val CALL_NOTIFICATION_ID = 102
const val CALL_MISSED_NOTIFICATION_ID = 103

View File

@ -7,33 +7,61 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.ContactsContract
import androidx.core.net.toUri
import com.tutpro.baresip.BaresipService.Companion.contactNames
import java.io.File
import java.util.ArrayList
sealed class Contact {
class BaresipContact(var name: String, var uri: String, var color: Int, val id: Long,
class BaresipContact(var name: String, var uri: String, var color: Int, var id: Long,
var favorite: Boolean): Contact() {
var avatarImage: Bitmap? = null
}
class AndroidContact(var name: String, var color: Int, var thumbnailUri: Uri?,
var favorite: Boolean): Contact() {
var id: Long, var favorite: Boolean): Contact() {
val uris = ArrayList<String>()
}
fun name(): String {
return when (this) {
is AndroidContact -> name
is BaresipContact -> name
}
}
fun id(): Long {
return when (this) {
is AndroidContact -> id
is BaresipContact -> id
}
}
fun favorite(): Boolean {
return when (this) {
is AndroidContact -> favorite
is BaresipContact -> favorite
}
}
fun copy(): Contact {
val copy = when (this) {
is BaresipContact ->
BaresipContact(name, uri, color, id, favorite)
is AndroidContact ->
AndroidContact(name, color, thumbnailUri, id, favorite)
}
when (this) {
is BaresipContact ->
(copy as BaresipContact).avatarImage = avatarImage
is AndroidContact ->
(copy as AndroidContact).uris.addAll(uris)
}
return copy
}
companion object {
const val CONTACTS_SIZE = 256
fun contacts(): ArrayList<Contact> {
return BaresipService.contacts
}
fun contactNames(): ArrayList<String> {
return BaresipService.contactNames
}
// Return contact name of uri or uri itself if contact with uri is not found
fun contactName(uri: String): String {
var contact = findContact(uri)
@ -42,21 +70,29 @@ sealed class Contact {
if (Utils.isTelNumber(userPart))
contact = findContact("tel:$userPart")
}
if (contact != null) {
return when (contact) {
is BaresipContact ->
contact.name
is AndroidContact ->
contact.name
}
}
if (contact != null)
return contact.name()
return uri
}
fun baresipContact(name: String): BaresipContact? {
for (c in BaresipService.baresipContacts.value)
if (c.name == name)
return c
return null
}
fun androidContact(name: String): AndroidContact? {
for (c in BaresipService.androidContacts.value)
if (c.name == name)
return c
return null
}
// Return URIs of contact name
fun contactUris(name: String): ArrayList<String> {
val uris = ArrayList<String>()
for (c in contacts())
for (c in BaresipService.contacts)
when (c) {
is BaresipContact -> {
if (c.name.equals(name, ignoreCase = true)) {
@ -78,7 +114,7 @@ sealed class Contact {
}
fun findContact(uri: String): Contact? {
for (c in contacts())
for (c in BaresipService.contacts)
when (c) {
is BaresipContact -> {
if (Utils.uriMatch(c.uri, uri))
@ -95,22 +131,16 @@ sealed class Contact {
return null
}
fun nameExists(name: String, ignoreCase: Boolean): Boolean {
for (c in BaresipService.contacts)
when (c) {
is BaresipContact ->
if (c.name.equals(name, ignoreCase = ignoreCase))
return true
is AndroidContact ->
if (c.name.equals(name, ignoreCase = ignoreCase))
return true
}
fun nameExists(name: String, list: List<Contact>, ignoreCase: Boolean): Boolean {
for (c in list)
if (c.name().equals(name, ignoreCase = ignoreCase))
return true
return false
}
fun saveBaresipContacts() {
var contents = ""
for (c in BaresipService.baresipContacts)
for (c in BaresipService.baresipContacts.value)
contents += "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}" +
";favorite=${if (c.favorite) "yes" else "no"}\n"
Utils.putFileContents(BaresipService.filesPath + "/contacts",
@ -129,7 +159,7 @@ sealed class Contact {
ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'"
val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection,
selection, null, null)
BaresipService.androidContacts.clear()
BaresipService.androidContacts.value = listOf()
val contacts = HashMap<Long, AndroidContact>()
while (cur != null && cur.moveToNext()) {
val id = cur.getLong(0)
@ -141,7 +171,7 @@ sealed class Contact {
val contact = if (contacts.containsKey(id))
contacts[id]!!
else
AndroidContact(name, Utils.randomColor(), thumb, starred == 1)
AndroidContact(name, Utils.randomColor(), thumb, id, starred == 1)
if (contact.name == "" && name != "")
contact.name = name
if (contact.thumbnailUri == null && thumb != null)
@ -158,9 +188,11 @@ sealed class Contact {
contacts[id] = contact
}
cur?.close()
val newList = mutableListOf<AndroidContact>()
for ((_, value) in contacts)
if (value.name != "" && value.uris.isNotEmpty())
BaresipService.androidContacts.add(value)
newList.add(value)
BaresipService.androidContacts.value = newList.toList()
}
@Suppress("unused")
@ -179,7 +211,8 @@ sealed class Contact {
val contacts = String(content)
var contactNo = 0
val baseId = System.currentTimeMillis()
BaresipService.baresipContacts.clear()
val restored = mutableListOf<BaresipContact>()
BaresipService.baresipContacts.value = listOf()
contacts.lines().forEach {
val parts = it.split("\"")
if (parts.size == 3) {
@ -212,55 +245,62 @@ sealed class Contact {
Log.e(TAG, "Could not read avatar image from '$id.png")
}
}
BaresipService.baresipContacts.add(contact)
restored.add(contact)
}
}
BaresipService.baresipContacts.value = restored.toList()
return true
}
fun contactsUpdate() {
BaresipService.contacts.clear()
val newList = mutableListOf<Contact>()
if (BaresipService.contactsMode != "android")
BaresipService.contacts.addAll(BaresipService.baresipContacts)
for (c in BaresipService.baresipContacts.value)
newList.add(c.copy())
if (BaresipService.contactsMode != "baresip")
addAndroidContacts()
sortContacts()
for (c in BaresipService.androidContacts.value)
if (!nameExists(c.name, newList, true))
newList.add(c.copy())
newList.sortBy{ when (it) {
is BaresipContact -> if (it.favorite) "0" + it.name else "1" + it.name
is AndroidContact -> if (it.favorite) "0" + it.name else "1" + it.name
}}
BaresipService.contacts = newList.toList()
generateContactNames()
BaresipService.contactUpdate.postValue(System.nanoTime())
//BaresipService.contactUpdate.postValue(System.nanoTime())
}
fun addBaresipContact(contact: BaresipContact) {
BaresipService.baresipContacts.add(contact)
val updatedList = BaresipService.baresipContacts.value.toMutableList()
updatedList.add(contact)
BaresipService.baresipContacts.value = updatedList.toList()
}
fun updateBaresipContact(contact: BaresipContact) {
val updatedList = BaresipService.baresipContacts.value.toMutableList()
val contactCopy = contact.copy()
updatedList.removeIf { it.id == contact.id }
updatedList.add(contactCopy as BaresipContact)
BaresipService.baresipContacts.value = updatedList.toList()
}
fun removeBaresipContact(contact: BaresipContact) {
BaresipService.baresipContacts.remove(contact)
val updatedList = BaresipService.baresipContacts.value.toMutableList()
updatedList.removeIf { it.id == contact.id }
BaresipService.baresipContacts.value = updatedList.toList()
saveBaresipContacts()
}
private fun generateContactNames () {
BaresipService.contactNames.clear()
val newList = mutableListOf<String>()
for (c in BaresipService.contacts)
when (c) {
is BaresipContact ->
BaresipService.contactNames.add(c.name)
newList.add(c.name)
is AndroidContact ->
BaresipService.contactNames.add(c.name)
newList.add(c.name)
}
contactNames.value = newList.toList()
}
private fun addAndroidContacts() {
for (c in BaresipService.androidContacts)
if (!nameExists(c.name, true))
BaresipService.contacts.add(c)
}
private fun sortContacts() {
BaresipService.contacts.sortBy{ when (it) {
is BaresipContact -> if (it.favorite) "0" + it.name else "1" + it.name
is AndroidContact -> if (it.favorite) "0" + it.name else "1" + it.name
}}
}
}
}

View File

@ -1,524 +0,0 @@
package com.tutpro.baresip
import android.app.Activity
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.graphics.drawable.BitmapDrawable
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.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import androidx.exifinterface.media.ExifInterface
import com.tutpro.baresip.databinding.ActivityContactBinding
import java.io.ByteArrayOutputStream
import java.io.File
class ContactActivity : AppCompatActivity() {
private lateinit var binding: ActivityContactBinding
private lateinit var layout: LinearLayout
private lateinit var textAvatarView: TextView
private lateinit var cardAvatarView: CardView
private lateinit var cardImageAvatarView: ImageView
private lateinit var nameView: EditText
private lateinit var uriView: EditText
private lateinit var favoriteCheck: CheckBox
private lateinit var androidCheck: CheckBox
private lateinit var menu: Menu
private var newContact = false
private var newAvatar = ""
private var uOrI = ""
private var index = 0
private var color = 0
private var id: Long = 0
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
goBack()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityContactBinding.inflate(layoutInflater)
setContentView(binding.root)
layout = binding.ContactView
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.ContactView.updatePadding(top = systemBars.top + 56)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
textAvatarView = binding.TextAvatar
cardAvatarView = binding.CardAvatar
cardImageAvatarView = binding.ImageAvatar
nameView = binding.Name
uriView = binding.Uri
favoriteCheck = binding.Favorite
androidCheck = binding.Android
newContact = intent.getBooleanExtra("new", false)
if (newContact) {
if (BaresipService.contactsMode == "baresip") {
binding.AndroidTitle.visibility = View.GONE
androidCheck.visibility = View.GONE
}
title = getString(R.string.new_contact)
color = Utils.randomColor()
id = System.currentTimeMillis()
showTextAvatar("?", color)
nameView.setText("")
nameView.hint = getString(R.string.contact_name)
nameView.setSelection(nameView.text.length)
val uri = intent.getStringExtra("uri")!!
if (uri == "")
uriView.setText("")
else
uriView.setText(uri)
uOrI = uri
favoriteCheck.isChecked = false
if ((BaresipService.contactsMode == "android")) {
androidCheck.isChecked = true
androidCheck.isClickable = false
} else {
androidCheck.isChecked = false
}
} else {
binding.AndroidTitle.visibility = View.GONE
androidCheck.visibility = View.GONE
index = intent.getIntExtra("index", 0)
val contact = Contact.contacts()[index] as Contact.BaresipContact
val name = contact.name
color = contact.color
id = contact.id
val avatarImage = contact.avatarImage
if (avatarImage != null)
showImageAvatar(avatarImage)
else
showTextAvatar(name, color)
title = name
nameView.setText(name)
uriView.setText(contact.uri)
favoriteCheck.isChecked = contact.favorite
uOrI = index.toString()
}
val avatarRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.also { uri ->
try {
val inputStream = baseContext.contentResolver.openInputStream(uri)
val avatarBitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val scaledBitmap = Bitmap.createScaledBitmap(avatarBitmap, 192, 192, true)
val exif = ExifInterface(baseContext.contentResolver.openInputStream(uri)!!)
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL)
val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
showImageAvatar(rotatedBitmap)
if (Utils.saveBitmap(rotatedBitmap, File(BaresipService.filesPath, "tmp.png")))
newAvatar = "image"
} catch (e: Exception) {
Log.e(TAG, "Could not read avatar image: $e")
}
}
}
}
textAvatarView.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
}
avatarRequest.launch(intent)
}
textAvatarView.setOnLongClickListener {
color = Utils.randomColor()
showTextAvatar(textAvatarView.text.toString(), color)
newAvatar = "text"
true
}
cardAvatarView.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
}
avatarRequest.launch(intent)
}
cardAvatarView.setOnLongClickListener {
color = Utils.randomColor()
showTextAvatar(nameView.text.toString(), color)
newAvatar = "text"
true
}
binding.AndroidTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.android),
getString(R.string.android_contact_help))
}
// Log.d(TAG, "Android sip contacts ${logAndroidContacts(this, "sip")}")
// Log.d(TAG, "Android tel contacts ${logAndroidContacts(this, "tel")}")
Utils.addActivity("contact,$newContact,$uOrI")
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
override fun onCreateOptionsMenu(optionsMenu: Menu): Boolean {
super.onCreateOptionsMenu(optionsMenu)
val inflater = menuInflater
inflater.inflate(R.menu.check_icon, optionsMenu)
menu = optionsMenu
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (BaresipService.activities.indexOf("contact,$newContact,$uOrI") == -1) return true
when (item.itemId) {
R.id.checkIcon -> {
var newName = nameView.text.toString().trim()
var newUri = uriView.text.toString().filterNot{setOf('-', ' ', '(', ')').contains(it)}
if (newName == "") newName = newUri
if (!Utils.checkName(newName)) {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.invalid_contact), newName))
return false
}
val alert: Boolean = if (newContact)
Contact.nameExists(newName, true)
else {
val c = Contact.contacts()[index] as Contact.BaresipContact
(c.name != newName) && Contact.nameExists(newName, false)
}
if (alert) {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.contact_already_exists), newName))
return false
}
if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
newUri = if (Utils.isTelNumber(newUri))
"tel:$newUri"
else
"sip:$newUri"
if (!Utils.checkUri(newUri)) {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.invalid_sip_or_tel_uri), newUri))
return false
}
val contact: Contact.BaresipContact
if (newContact) {
if (Contact.contacts().size >= Contact.CONTACTS_SIZE) {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.contacts_exceeded),
Contact.CONTACTS_SIZE))
BaresipService.activities.removeAt(0)
return true
} else {
contact = Contact.BaresipContact(newName, newUri, color, id, favoriteCheck.isChecked)
}
} else {
contact = Contact.contacts()[index] as Contact.BaresipContact
contact.uri = newUri
contact.name = newName
contact.color = color
contact.favorite = favoriteCheck.isChecked
}
when (newAvatar) {
"text" -> {
if (contact.avatarImage != null) {
contact.avatarImage = null
Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png"))
}
}
"image" -> {
contact.avatarImage = (cardImageAvatarView.drawable as BitmapDrawable).bitmap
Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png"))
File(BaresipService.filesPath, "tmp.png")
.renameTo(File(BaresipService.filesPath, "${contact.id}.png"))
}
}
if (androidCheck.isChecked) {
addOrUpdateAndroidContact(this, contact)
} else {
if (newContact)
Contact.addBaresipContact(contact)
Contact.saveBaresipContacts()
Contact.contactsUpdate()
}
BaresipService.activities.remove("contact,$newContact,$uOrI")
val i = Intent(this, MainActivity::class.java)
if (androidCheck.isChecked && contact.favorite)
i.putExtra("name", newName)
setResult(Activity.RESULT_OK, i)
finish()
}
android.R.id.home -> {
goBack()
}
}
return true
}
private fun goBack() {
BaresipService.activities.remove("contact,$newContact,$uOrI")
setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java))
finish()
}
private fun showTextAvatar(name: String, color: Int) {
textAvatarView.visibility = View.VISIBLE
cardAvatarView.visibility = View.GONE
textAvatarView.background.setTint(color)
textAvatarView.text = "${name[0]}"
}
private fun showImageAvatar(image: Bitmap) {
textAvatarView.visibility = View.GONE
cardAvatarView.visibility = View.VISIBLE
cardImageAvatarView.setImageBitmap(image)
}
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")
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")
}
}
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")
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")
}
}
}
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")
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")
null
}
}
}

View File

@ -1,218 +0,0 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle
import android.os.SystemClock
import android.provider.ContactsContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
import java.io.IOException
class ContactListAdapter(private val ctx: Context, private val rows: ArrayList<Contact>,
private val aor: String) :
ArrayAdapter<Contact>(ctx, R.layout.contact_row, rows) {
private val layoutInflater = LayoutInflater.from(context)
private var lastClick: Long = 0
private class ViewHolder(view: View?) {
val textAvatarView: TextView = view?.findViewById(R.id.TextAvatar)!!
val imageAvatarView: ImageView = view?.findViewById(R.id.ImageAvatar)!!
val nameView: TextView = view?.findViewById(R.id.contactName)!!
val actionView: ImageButton = view?.findViewById(R.id.edit)!!
}
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
if (view == null) {
rowView = layoutInflater.inflate(R.layout.contact_row, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val contact = rows[position]
if (contact is Contact.BaresipContact) {
val avatarImage = contact.avatarImage
if (avatarImage != null) {
viewHolder.imageAvatarView.setImageBitmap(avatarImage)
} else {
viewHolder.textAvatarView.background.setTint(contact.color)
if (contact.name.isNotEmpty())
viewHolder.textAvatarView.text = "${contact.name[0]}"
else
viewHolder.textAvatarView.text = ""
viewHolder.imageAvatarView.setImageBitmap(Utils.bitmapFromView(viewHolder.textAvatarView))
}
viewHolder.nameView.text = contact.name
viewHolder.nameView.textSize = 20f
if (contact.favorite)
viewHolder.nameView.setTypeface(null, Typeface.ITALIC)
else
viewHolder.nameView.setTypeface(null, Typeface.NORMAL)
viewHolder.nameView.setPadding(6, 6, 0, 6)
if (aor != "") {
viewHolder.nameView.setOnClickListener {
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
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
if (which == DialogInterface.BUTTON_NEGATIVE)
i.putExtra("action", "call")
else
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)
}
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime()
with(MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)) {
setTitle(R.string.confirmation)
setMessage(String.format(ctx.getString(R.string.contact_action_question),
contact.name))
setNeutralButton(ctx.getText(R.string.cancel), dialogClickListener)
setNegativeButton(ctx.getText(R.string.call), dialogClickListener)
setPositiveButton(ctx.getText(R.string.send_message), dialogClickListener)
show()
}
}
}
}
viewHolder.actionView.visibility = View.VISIBLE
viewHolder.actionView.setOnClickListener {
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime()
val i = Intent(ctx, ContactActivity::class.java)
val b = Bundle()
b.putBoolean("new", false)
b.putInt("index", position)
i.putExtras(b)
ctx.startActivity(i, null)
}
}
}
if (contact is Contact.AndroidContact) {
val thumbNailUri = contact.thumbnailUri
if (thumbNailUri != null) {
viewHolder.imageAvatarView.setImageURI(thumbNailUri)
} else {
viewHolder.textAvatarView.background.setTint(contact.color)
if (contact.name.isNotEmpty())
viewHolder.textAvatarView.text = "${contact.name[0]}"
else
viewHolder.textAvatarView.text = ""
viewHolder.imageAvatarView.setImageBitmap(Utils.bitmapFromView(viewHolder.textAvatarView))
}
viewHolder.nameView.text = contact.name
viewHolder.nameView.textSize = 20f
if (contact.favorite)
viewHolder.nameView.setTypeface(null, Typeface.ITALIC)
else
viewHolder.nameView.setTypeface(null, Typeface.NORMAL)
viewHolder.nameView.setPadding(6, 6, 0, 6)
viewHolder.nameView.setOnClickListener {
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime()
val i = Intent(ctx, AndroidContactActivity::class.java)
val b = Bundle()
b.putString("aor", aor)
b.putInt("index", position)
i.putExtras(b)
ctx.startActivity(i, null)
}
}
viewHolder.actionView.visibility = View.GONE
}
viewHolder.nameView.setOnLongClickListener {
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
when (contact) {
is Contact.BaresipContact -> {
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")
}
}
Contact.removeBaresipContact(contact)
Contact.contactsUpdate()
this.notifyDataSetChanged()
}
is Contact.AndroidContact -> {
deleteAndroidContact(ctx, contact.name)
}
}
}
DialogInterface.BUTTON_NEGATIVE -> {
}
}
}
with(MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)) {
setTitle(R.string.confirmation)
setMessage(String.format(ctx.getString(R.string.contact_delete_question),
when (contact) {
is Contact.BaresipContact -> contact.name
is Contact.AndroidContact -> contact.name
}))
setNeutralButton(ctx.getText(R.string.cancel), dialogClickListener)
setPositiveButton(ctx.getText(R.string.delete), dialogClickListener)
show()
}
true
}
return rowView
}
private fun deleteAndroidContact(ctx: Context, name: String): Int {
return ctx.contentResolver.delete(ContactsContract.RawContacts.CONTENT_URI,
ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
null)
}
}

View File

@ -1,28 +1,71 @@
package com.tutpro.baresip
import android.app.Activity
import android.content.*
import android.os.Build
import android.content.ContentValues
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.SystemClock
import android.provider.ContactsContract
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem
import android.view.View
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.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
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.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 com.tutpro.baresip.databinding.ActivityContactsBinding
import coil.compose.AsyncImage
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tutpro.baresip.CustomElements.TextAvatar
import com.tutpro.baresip.CustomElements.verticalScrollbar
import java.io.File
import java.io.IOException
class ContactsActivity : AppCompatActivity() {
class ContactsActivity : ComponentActivity() {
private lateinit var binding: ActivityContactsBinding
private lateinit var clAdapter: ContactListAdapter
private lateinit var aor: String
private var newAndroidName: String? = null
private var lastClick: Long = 0
@ -33,32 +76,26 @@ class ContactsActivity : AppCompatActivity() {
}
}
private val contactRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
if (it.data != null && it.data!!.hasExtra("name"))
newAndroidName = it.data!!.getStringExtra("name")
}
Contact.contactsUpdate()
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityContactsBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
if (Build.VERSION.SDK_INT >= 35)
binding.ContactsView.updatePadding(top = systemBars.top + 56)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
val title = String.format(getString(R.string.contacts))
aor = intent.getStringExtra("aor")!!
Utils.addActivity("contacts,$aor")
val listView = binding.contacts
clAdapter = ContactListAdapter(this, BaresipService.contacts, aor)
listView.adapter = clAdapter
listView.isLongClickable = true
val androidContactsObserver = Observer<Long> {
if (newAndroidName != null) {
val contentValues = ContentValues()
@ -73,64 +110,374 @@ class ContactsActivity : AppCompatActivity() {
}
newAndroidName = null
}
clAdapter.notifyDataSetChanged()
Contact.contactsUpdate()
}
BaresipService.contactUpdate.observe(this, androidContactsObserver)
val contactRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
if (it.data != null && it.data!!.hasExtra("name"))
newAndroidName = it.data!!.getStringExtra("name")
}
clAdapter.notifyDataSetChanged()
}
val plusButton = binding.plusButton
plusButton.setOnClickListener {
if (Contact.contacts().size >= Contact.CONTACTS_SIZE) {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.contacts_exceeded),
Contact.CONTACTS_SIZE))
} else {
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime()
val intent = Intent(this, ContactActivity::class.java)
val b = Bundle()
b.putBoolean("new", true)
b.putString("uri", "")
intent.putExtras(b)
contactRequest.launch(intent)
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
ContactsScreen(LocalContext.current, title) {
goBack()
}
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
override fun onResume() {
super.onResume()
clAdapter.notifyDataSetChanged()
@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)
}
)
}
@Composable
fun ContactsContent(ctx: Context, contentPadding: PaddingValues) {
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 = { contact -> contact.id() }) { contact ->
val name = contact.name()
Row(
verticalAlignment = Alignment.CenterVertically,
) {
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 dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
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
if (which == DialogInterface.BUTTON_NEGATIVE)
i.putExtra("action", "call")
else
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)
}
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime()
with(
MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
) {
setTitle(R.string.confirmation)
setMessage(
String.format(
ctx.getString(R.string.contact_action_question),
name
)
)
setNeutralButton(
ctx.getText(R.string.cancel),
dialogClickListener
)
setNegativeButton(
ctx.getText(R.string.call),
dialogClickListener
)
setPositiveButton(
ctx.getText(R.string.send_message),
dialogClickListener
)
show()
}
}
},
onLongClick = {
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
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"
)
}
}
Contact.removeBaresipContact(contact)
Contact.contactsUpdate()
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
with(
MaterialAlertDialogBuilder(
ctx,
R.style.AlertDialogTheme
)
) {
setTitle(R.string.confirmation)
setMessage(
String.format(
ctx.getString(R.string.contact_delete_question),
name
)
)
setNeutralButton(
ctx.getText(R.string.cancel),
dialogClickListener
)
setPositiveButton(
ctx.getText(R.string.delete),
dialogClickListener
)
show()
}
}
)
)
SmallFloatingActionButton(
modifier = Modifier.padding(end = 16.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.add)
)
}
}
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 = {
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
ctx.contentResolver.delete(
ContactsContract.RawContacts.CONTENT_URI,
ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
null
)
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
with(
MaterialAlertDialogBuilder(
ctx,
R.style.AlertDialogTheme
)
) {
setTitle(R.string.confirmation)
setMessage(
String.format(
ctx.getString(R.string.contact_delete_question),
name
)
)
setNeutralButton(
ctx.getText(R.string.cancel),
dialogClickListener
)
setPositiveButton(
ctx.getText(R.string.delete),
dialogClickListener
)
show()
}
}
)
)
}
}
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home ->
goBack()
}
return true
}
private fun goBack() {
BaresipService.activities.remove("contacts,$aor")
setResult(Activity.RESULT_OK, Intent())
setResult(RESULT_OK, Intent())
finish()
}
}

View File

@ -0,0 +1,177 @@
package com.tutpro.baresip
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.compositionLocalOf
@Immutable
data class CustomColors(
val primary: Color = Color.Unspecified,
val primaryDark: Color = Color.Unspecified,
val primaryLight: Color = Color.Unspecified,
val secondary: Color = Color.Unspecified,
val secondaryDark: Color = Color.Unspecified,
val secondaryLight: Color = Color.Unspecified,
val gray: Color = Color.Unspecified,
val grayDark: Color = Color.Unspecified,
val grayLight: Color = Color.Unspecified,
val accent: Color = Color.Unspecified,
val baresip: Color = Color.Unspecified,
val white: Color = Color.Unspecified,
val black: Color = Color.Unspecified,
val strong: Color = Color.Unspecified,
val green: Color = Color.Unspecified,
val red: Color = Color.Unspecified,
val trafficGreen: Color = Color.Unspecified,
val trafficYellow: Color = Color.Unspecified,
val trafficRed: Color = Color.Unspecified,
val light: Color = Color.Unspecified,
val dark: Color = Color.Unspecified,
val spinnerText: Color = Color.Unspecified,
val spinnerDropdown: Color = Color.Unspecified,
val spinnerDivider: Color = Color.Unspecified,
val itemText: Color = Color.Unspecified,
val alertText: Color = Color.Unspecified,
val codec: Color = Color.Unspecified,
val background: Color = Color.Unspecified,
val popupBackground: Color = Color.Unspecified,
val alert: Color = Color.Unspecified,
val actionbar: Color = Color.Unspecified,
)
val light_primary = Color(0xff0ca1fd)
val light_primary_dark = Color(0xff0073c9)
val light_primary_light = Color(0xff6ad2ff)
val light_secondary = Color(0xFF00B9A1)
val light_secondary_dark = Color(0xFF008873)
val light_secondary_light = Color(0xff5cecd2)
val light_gray = Color(0xff9e9e9e)
val light_gray_dark = Color(0xFF424242)
val light_gray_light = Color(0xffe0e0e0)
val light_accent = Color(0xFFF77445)
val light_baresip = Color(0xff0ca1fd)
val light_white = Color(0xffffffff)
val light_black = Color(0xFF000000)
val light_strong = light_black
val light_green = Color(0xff01df01)
val light_red = Color(0xffff0000)
val light_traffic_green = Color(0xFF4CAF50)
val light_traffic_yellow = Color(0xFFFFEB3B)
val light_traffic_red = Color(0xFFFF5722)
val light_light = light_white
val light_dark = Color(0xFF383838)
val light_spinner_text = light_dark
val light_spinner_dropdown = light_gray_light
val light_spinner_divider = light_gray
val light_item_text = light_black
val light_alert_text = light_black
val light_codec = light_black
val light_background = Color(0xfffcfcfc)
val light_popup_background = light_secondary_dark
val light_alert = Color(0xff0ca1fd) // == light_primary
val light_actionbar = light_background
val dark_primary = Color(0xff0073c9)
val dark_primary_dark = Color(0xff00008b)
val dark_primary_light = Color(0xff0ca1fd)
val dark_secondary = Color(0xFF00B9A1)
val dark_secondary_dark = Color(0xFF008873)
val dark_secondary_light = Color(0xff5cecd2)
val dark_gray = Color(0xff9e9e9e)
val dark_gray_dark = Color(0xFF424242)
val dark_gray_light = Color(0xffe0e0e0)
val dark_accent = Color(0xFFF77445)
val dark_baresip = Color(0xff0ca1fd)
val dark_white = Color(0xffffffff)
val dark_black = Color(0xFF000000)
val dark_strong = dark_white
val dark_green = Color(0xff01df01)
val dark_red = Color(0xffdf0101)
val dark_traffic_green = Color(0xFF2E7D32)
val dark_traffic_yellow = Color(0xFFF9A825)
val dark_traffic_red = Color(0xFFC62828)
val dark_light = dark_gray_light
val dark_dark = Color(0xFF383838)
val dark_spinner_text = dark_dark
val dark_spinner_dropdown = dark_gray
val dark_spinner_divider = dark_gray_dark
val dark_item_text = dark_gray_light
val dark_alert_text = dark_gray_light
val dark_codec = dark_secondary_dark
val dark_background = dark_dark
val dark_popup_background = dark_secondary_dark
val dark_alert = dark_primary_light
val dark_actionbar = dark_gray_dark
val LightCustomColors = CustomColors(
primary = light_primary,
primaryDark = light_primary_dark,
primaryLight = light_primary_light,
secondary = light_secondary,
secondaryDark = light_secondary_dark,
secondaryLight = light_secondary_light,
gray = light_gray,
grayDark = light_gray_dark,
grayLight = light_gray_light,
accent = light_accent,
baresip = light_baresip,
white = light_white,
black = light_black,
strong = light_strong,
green = light_green,
red = light_red,
trafficGreen = light_traffic_green,
trafficYellow = light_traffic_yellow,
trafficRed = light_traffic_red,
light = light_light,
dark = light_dark,
spinnerText = light_spinner_text,
spinnerDropdown = light_spinner_dropdown,
spinnerDivider = light_spinner_divider,
itemText = light_item_text,
alertText = light_alert_text,
codec = light_codec,
background = light_background,
popupBackground = light_popup_background,
alert = light_alert,
actionbar = light_actionbar,
)
val DarkCustomColors = CustomColors(
primary = dark_primary,
primaryDark = dark_primary_dark,
primaryLight = dark_primary_light,
secondary = dark_secondary,
secondaryDark = dark_secondary_dark,
secondaryLight = dark_secondary_light,
gray = dark_gray,
grayDark = dark_gray_dark,
grayLight = dark_gray_light,
accent = dark_accent,
baresip = dark_baresip,
white = dark_white,
black = dark_black,
strong = dark_strong,
green = dark_green,
red = dark_red,
trafficGreen = dark_traffic_green,
trafficYellow = dark_traffic_yellow,
trafficRed = dark_traffic_red,
light = dark_light,
dark = dark_dark,
spinnerText = dark_spinner_text,
spinnerDropdown = dark_spinner_dropdown,
spinnerDivider = dark_spinner_divider,
itemText = dark_item_text,
alertText = dark_alert_text,
codec = dark_codec,
background = dark_background,
popupBackground = dark_popup_background,
alert = dark_alert,
actionbar = dark_actionbar,
)
val LocalCustomColors = compositionLocalOf { CustomColors() }

View File

@ -0,0 +1,317 @@
package com.tutpro.baresip
import android.content.Context
import android.graphics.Bitmap
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.TextButton
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
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.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat.getString
import com.tutpro.baresip.CustomElements.Text
import com.tutpro.baresip.CustomElements.ThreeButtonAlertDialog
object CustomElements {
@Composable
fun Text(
text: String,
modifier: Modifier = Modifier,
color: Color = LocalCustomColors.current.itemText,
fontSize: TextUnit = 16.sp,
fontWeight: FontWeight = FontWeight.Normal,
textAlign: TextAlign? = null,
maxLines: Int = Int.MAX_VALUE,
) {
androidx.compose.material3.Text(
text = text,
modifier = modifier,
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
textAlign = textAlign,
maxLines = maxLines
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PullToRefreshBox(
isRefreshing: Boolean,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
enabled: Boolean = true,
content: @Composable BoxScope.() -> Unit,
) {
val refreshState = rememberPullToRefreshState()
Box(
modifier.pullToRefresh(
state = refreshState,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
enabled = enabled,
),
contentAlignment = contentAlignment,
) {
content()
Indicator(
modifier = Modifier.align(Alignment.TopCenter),
isRefreshing = isRefreshing,
state = refreshState,
)
}
}
@Composable
fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
items: List<String>,
onItemClick: (String) -> Unit
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
containerColor = LocalCustomColors.current.grayLight,
modifier = Modifier.background(
LocalCustomColors.current.popupBackground.copy(alpha = 0.95f)
)
) {
val itemsIterator = items.iterator()
while (itemsIterator.hasNext()) {
val item = itemsIterator.next()
DropdownMenuItem(
text = { Text(item, color = LocalCustomColors.current.light) },
onClick = { onItemClick(item) }
)
if (itemsIterator.hasNext())
HorizontalDivider(color = LocalCustomColors.current.spinnerDivider)
}
}
}
@Composable
fun Checkbox(checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
Checkbox(
checked = checked,
onCheckedChange = {
onCheckedChange(!checked)
},
colors = CheckboxDefaults.colors(
checkedColor = LocalCustomColors.current.accent,
uncheckedColor = LocalCustomColors.current.strong,
checkmarkColor = LocalCustomColors.current.strong,
)
)
}
@Composable
fun TextAvatar(name: String, color: Int) {
Box(
modifier = Modifier.size(36.dp),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(SolidColor(Color(color)))
}
val text = if (name == "") "" else name[0].toString()
Text(text, color = Color.White, fontSize = 20.sp)
}
}
@Composable
fun ImageAvatar(bitmap: Bitmap) {
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = "Avatar",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
)
}
@Composable
fun DrawDrawable(
drawableId: Int,
modifier: Modifier = Modifier,
contentDescription: String? = null,
tint: Color? = null
) {
val image = painterResource(id = drawableId)
Image(
painter = image,
contentDescription = contentDescription,
modifier = modifier,
colorFilter = if (tint != null) ColorFilter.tint(tint) else null
)
}
@Composable
fun Modifier.verticalScrollbar(
state: LazyListState,
width: Dp = 8.dp,
alwaysShow: Boolean = true,
color: Color = LocalCustomColors.current.gray
): Modifier {
val targetAlpha = if (state.isScrollInProgress || alwaysShow) 1f else 0f
val duration = if (state.isScrollInProgress) 150 else 500
val alpha by animateFloatAsState(
targetValue = targetAlpha,
animationSpec = tween(durationMillis = duration)
)
return drawWithContent {
drawContent()
val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index
val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f
// Draw scrollbar if scrolling or if the animation is still running and lazy column has content
if (needDrawScrollbar && firstVisibleElementIndex != null) {
val elementHeight = this.size.height / state.layoutInfo.totalItemsCount
val scrollbarOffsetY = firstVisibleElementIndex * elementHeight
val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight
drawRect(
color = color,
topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY),
size = Size(width.toPx(), scrollbarHeight),
alpha = alpha
)
}
}
}
@Composable
fun ThreeButtonAlertDialog(
onDismissRequest: () -> Unit,
onPositiveClick: () -> Unit,
onNegativeClick: () -> Unit,
onNeutralClick: () -> Unit,
dialogTitle: String,
dialogText: String,
) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(
modifier = Modifier
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
modifier = Modifier.padding(bottom = 16.dp),
text = dialogTitle
)
Text(
modifier = Modifier.padding(bottom = 16.dp),
text = dialogText
)
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(
onClick = { onNeutralClick() },
modifier = Modifier.weight(1f)
) {
Text("Neutral")
}
TextButton(
onClick = { onNegativeClick() },
modifier = Modifier.weight(1f)
) {
Text("Negative")
}
Button(
onClick = { onPositiveClick() },
modifier = Modifier.weight(1f)
) {
Text("Positive")
}
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ThreeButtonAlertDialogPreview() {
var openDialog by remember { mutableStateOf(true) }
if (openDialog) {
ThreeButtonAlertDialog(
onDismissRequest = { openDialog = false },
onPositiveClick = { openDialog = false },
onNegativeClick = { openDialog = false },
onNeutralClick = { openDialog = false },
dialogTitle = "My Dialog Title",
dialogText = "This is the message text of my dialog.",
)
}
}

View File

@ -0,0 +1,358 @@
/*
* Copyright 2024 Allan Veloso Lopes
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Temporarily implementation until this issue is made available on the official library
// https://issuetracker.google.com/issues/356619939
// Based on https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
// https://issuetracker.google.com/issues/181282427
package com.tutpro.baresip
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
/**
* Creates a [DraggableListState] that is remembered across compositions.
*
* @param lazyListState the [LazyListState] whose items are being dragged.
* @param onMove the callback that is triggered when an item is moved.
*/
@Composable
fun rememberDraggableListState(
lazyListState: LazyListState = rememberLazyListState(),
onMove: (Int, Int) -> Unit
): DraggableListState {
val scope = rememberCoroutineScope()
val state = remember(lazyListState) {
DraggableListState(
listState = lazyListState,
onMove = onMove,
scope = scope
)
}
LaunchedEffect(state) {
while (true) {
val diff = state.scrollChannel.receive()
lazyListState.scrollBy(diff)
}
}
return state
}
/**
* A state object that can be hoisted to control and observe dragging within a list.
*
* In most cases, this will be created via [rememberLazyListState].
*
* @param listState the state of the list where the item is being dragged.
* @param scope a coroutine scope for performing animations.
* @param onMove the callback that is triggered when an item is moved. The callback is
* invoked whenever the dragged item middle point passes another item top or bottom offset,
* it does not depend on the drag to be completed.
*/
class DraggableListState internal constructor(
val listState: LazyListState,
private val scope: CoroutineScope,
private val onMove: (Int, Int) -> Unit
) {
var draggingItemIndex by mutableStateOf<Int?>(null)
private set
internal val scrollChannel = Channel<Float>()
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
private var draggingItemInitialOffset by mutableIntStateOf(0)
val draggingItemOffset: Float
get() = draggingItemLayoutInfo?.let { item ->
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
} ?: 0f
private val draggingItemLayoutInfo: LazyListItemInfo?
get() = listState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
var previousItemOffset = Animatable(0f)
private set
internal fun onDragStart(index: Int) {
listState.layoutInfo.visibleItemsInfo
.find { it.index == index }
?.also {
draggingItemIndex = it.index
draggingItemInitialOffset = it.offset
}
}
internal fun onDragStart(key: Any) {
listState.layoutInfo.visibleItemsInfo
.find { it.key == key }
?.also {
draggingItemIndex = it.index
draggingItemInitialOffset = it.offset
}
}
internal fun onDragInterrupted() {
if (draggingItemIndex != null) {
previousIndexOfDraggedItem = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
previousItemOffset.snapTo(startOffset)
previousItemOffset.animateTo(
0f,
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
)
previousIndexOfDraggedItem = null
}
}
draggingItemDraggedDelta = 0f
draggingItemIndex = null
draggingItemInitialOffset = 0
}
internal fun onDrag(offset: Offset) {
draggingItemDraggedDelta += offset.y
val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset + draggingItemOffset
val endOffset = startOffset + draggingItem.size
val middleOffset = startOffset + (endOffset - startOffset) / 2f
val targetItem = listState.layoutInfo.visibleItemsInfo.find { item ->
middleOffset.toInt() in item.offset..item.offsetEnd &&
draggingItem.index != item.index
}
if (targetItem != null) {
if (
draggingItem.index == listState.firstVisibleItemIndex ||
targetItem.index == listState.firstVisibleItemIndex
) {
listState.requestScrollToItem(
listState.firstVisibleItemIndex,
listState.firstVisibleItemScrollOffset
)
}
onMove(draggingItem.index, targetItem.index)
draggingItemIndex = targetItem.index
} else {
val overscroll = when {
draggingItemDraggedDelta > 0 ->
(endOffset - listState.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
draggingItemDraggedDelta < 0 ->
(startOffset - listState.layoutInfo.viewportStartOffset).coerceAtMost(0f)
else -> 0f
}
if (overscroll != 0f) {
scrollChannel.trySend(overscroll)
}
}
}
private val LazyListItemInfo.offsetEnd: Int
get() = this.offset + this.size
}
/**
* Defines the area that will act as the handle for dragging operations.
*
* @param state The draggable state of the list where the handler is being used.
* @param index The index of the item for which the handler is being used.
* @param onlyAfterLongPress Indicates whether dragging should begin only after a long press.
*/
fun Modifier.dragHandle(
state: DraggableListState,
index: Int,
onlyAfterLongPress: Boolean = false
): Modifier {
return pointerInput(state) {
if (onlyAfterLongPress) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
state.onDrag(offset = offset)
},
onDragStart = { state.onDragStart(index = index) },
onDragEnd = { state.onDragInterrupted() },
onDragCancel = { state.onDragInterrupted() }
)
} else {
detectDragGestures(
onDrag = { change, offset ->
change.consume()
state.onDrag(offset = offset)
},
onDragStart = { state.onDragStart(index = index) },
onDragEnd = { state.onDragInterrupted() },
onDragCancel = { state.onDragInterrupted() }
)
}
}
}
/**
* Defines the area that will act as the handle for dragging operations.
*
* If you use this version of the [dragHandle], you must implement the key parameter on the
* [draggableItems] or [draggableItemsIndexed].
*
* @param state The draggable state of the list where the handler is being used.
* @param key The unique identifier for the item within the list for which the handler is being used.
* @param onlyAfterLongPress Indicates whether dragging should begin only after a long press.
*/
fun Modifier.dragHandle(
state: DraggableListState,
key: Any,
onlyAfterLongPress: Boolean = false
): Modifier {
return pointerInput(state) {
if (onlyAfterLongPress) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
state.onDrag(offset = offset)
},
onDragStart = { state.onDragStart(key = key) },
onDragEnd = { state.onDragInterrupted() },
onDragCancel = { state.onDragInterrupted() }
)
} else {
detectDragGestures(
onDrag = { change, offset ->
change.consume()
state.onDrag(offset = offset)
},
onDragStart = { state.onDragStart(key = key) },
onDragEnd = { state.onDragInterrupted() },
onDragCancel = { state.onDragInterrupted() }
)
}
}
}
/**
* Adds a list of draggable items.
*
* To the items to be draggable, it's also necessary to add the modifier [dragHandle] to
* the preferable UI element of the item which will be the target of the drag operation.
*
* @param items the data list
* @param key a factory of stable and unique keys representing the item. Using the same key
* for multiple items in the list is not allowed. Type of the key should be saveable
* via Bundle on Android. If null is passed the position in the list will represent the key.
* When you specify the key the scroll position will be maintained based on the key, which
* means if you add/remove items before the current visible item the item with the given key
* will be kept as the first visible one. This can be overridden by calling 'requestScrollToItem'
* on the 'LazyListState'.
* @param contentType a factory of the content types for the item. The item compositions of
* the same type could be reused more efficiently. Note that null is a valid type and items of such
* type will be considered compatible.
* @param itemContent the content displayed by a single item
*/
inline fun <T> LazyListScope.draggableItemsIndexed(
state: DraggableListState,
items: List<T>,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T, isDragging: Boolean) -> Unit
) = itemsIndexed(
items = items,
key = key,
contentType = contentType
) { index, item ->
val isDragging = index == state.draggingItemIndex
val draggingModifier = if (isDragging) {
Modifier
.zIndex(1f)
.graphicsLayer { translationY = state.draggingItemOffset }
} else if (index == state.previousIndexOfDraggedItem) {
Modifier
.zIndex(1f)
.graphicsLayer { translationY = state.previousItemOffset.value }
} else {
Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
}
Box(modifier = draggingModifier) {
itemContent(index, item, isDragging)
}
}
/**
* Adds a list of draggable items.
*
* To the items to be draggable, it's also necessary to add the modifier [dragHandle] to
* the preferable UI element of the item which will be the target of the drag operation.
*
* @param items the data list
* @param key a factory of stable and unique keys representing the item. Using the same key
* for multiple items in the list is not allowed. Type of the key should be saveable
* via Bundle on Android. If null is passed the position in the list will represent the key.
* When you specify the key the scroll position will be maintained based on the key, which
* means if you add/remove items before the current visible item the item with the given key
* will be kept as the first visible one. This can be overridden by calling 'requestScrollToItem'
* on the 'LazyListState'.
* @param contentType a factory of the content types for the item. The item compositions of
* the same type could be reused more efficiently. Note that null is a valid type and items of such
* type will be considered compatible.
* @param itemContent the content displayed by a single item
*/
inline fun <T> LazyListScope.draggableItems(
state: DraggableListState,
items: List<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline contentType: (item: T) -> Any? = { _ -> null },
crossinline itemContent: @Composable LazyItemScope.(item: T, isDragging: Boolean) -> Unit
) = draggableItemsIndexed(
state = state,
items = items,
key = key?.let { block -> { _, item -> block(item) } },
contentType = { _, item -> contentType(item) },
itemContent = { _, item, isDragging -> itemContent(item, isDragging) }
)

File diff suppressed because it is too large Load Diff

View File

@ -8,32 +8,76 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
var new: Boolean): Serializable {
fun add() {
BaresipService.messages.add(this)
val updatedMessages = BaresipService.messages.toMutableList()
updatedMessages.add(this)
BaresipService.messages = updatedMessages.toList()
var count = 0
var firstIndex = -1
for (i in BaresipService.messages.indices)
if (BaresipService.messages[i].aor == this.aor) {
if (count == 0) firstIndex = i
var remove: Message? = null
for (message in BaresipService.messages)
if (message.aor == this.aor) {
count++
if (count > MESSAGE_HISTORY_SIZE) {
remove = message
break
}
}
if (count > MESSAGE_HISTORY_SIZE)
BaresipService.messages.removeAt(firstIndex)
if (remove != null)
updatedMessages.remove(remove)
BaresipService.messages = updatedMessages.toList()
save()
}
fun delete() {
val updatedMessages = BaresipService.messages.toMutableList()
updatedMessages.remove(this)
BaresipService.messages = updatedMessages.toList()
save()
}
companion object {
const val MESSAGE_HISTORY_SIZE = 100
fun messages(): ArrayList<Message> {
fun messages(): List<Message> {
return BaresipService.messages
}
fun clear(aor: String) {
val it = BaresipService.messages.iterator()
fun clearMessagesOfAor(aor: String) {
val updatedMessages = BaresipService.messages.toMutableList()
val it = updatedMessages.iterator()
while (it.hasNext()) if (it.next().aor == aor) it.remove()
BaresipService.messages = updatedMessages.toList()
}
fun deleteAorMessage(aor: String, time: Long) {
val updatedMessages = BaresipService.messages.toMutableList()
for (message in updatedMessages)
if (message.aor == aor && message.timeStamp == time) {
updatedMessages.remove(message)
BaresipService.messages = updatedMessages.toList()
save()
return
}
}
fun deleteAorPeerMessages(aor: String, peerUri: String) {
val updatedMessages = BaresipService.messages.toMutableList()
for (message in updatedMessages)
if (message.aor == aor && message.peerUri == peerUri)
updatedMessages.remove(message)
BaresipService.messages = updatedMessages.toList()
save()
}
fun updateAorMessage(aor: String, time: Long) {
val updatedMessages = BaresipService.messages.toMutableList()
for (message in updatedMessages)
if (message.aor == aor && message.timeStamp == time) {
message.new = false
BaresipService.messages = updatedMessages.toList()
save()
return
}
}
fun save() {

View File

@ -1,141 +0,0 @@
package com.tutpro.baresip
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle
import android.text.format.DateUtils.isToday
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.text.DateFormat
import java.util.*
class MessageListAdapter(private val ctx: Context, private val peerUri: String,
private val chatPeer: String, private val rows: ArrayList<Message>) :
ArrayAdapter<Message>(ctx, R.layout.message, rows) {
private val layoutInflater = LayoutInflater.from(context)
private class ViewHolder(view: View?) {
val layoutView: LinearLayout = view?.findViewById(R.id.message)!!
val infoView: TextView = view?.findViewById(R.id.info)!!
val textView: TextView = view?.findViewById(R.id.text)!!
}
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
if (view == null) {
rowView = layoutInflater.inflate(R.layout.message, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val listener = View.OnClickListener {
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
val i = Intent(ctx, ContactActivity::class.java)
val b = Bundle()
b.putBoolean("new", true)
b.putString("uri", peerUri)
i.putExtras(b)
ctx.startActivity(i)
}
DialogInterface.BUTTON_NEGATIVE -> {
Message.messages().remove(rows[position])
Message.save()
rows.removeAt(position)
this.notifyDataSetChanged()
//if (rows.size == 0) {
// listView.removeFooterView(footerView)
//}
}
DialogInterface.BUTTON_NEUTRAL -> {
}
}
}
val builder = MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
if (chatPeer == peerUri)
with (builder) {
setTitle(R.string.confirmation)
setMessage(String.format(ctx.getString(R.string.long_message_question),
peerUri))
setNeutralButton(ctx.getString(R.string.cancel), dialogClickListener)
setNegativeButton(ctx.getString(R.string.delete), dialogClickListener)
setPositiveButton(ctx.getString(R.string.add_contact), dialogClickListener)
show()
}
else
with (builder) {
setTitle(R.string.confirmation)
setMessage(ctx.getText(R.string.short_message_question))
setNeutralButton(ctx.getString(R.string.cancel), dialogClickListener)
setNegativeButton(ctx.getString(R.string.delete), dialogClickListener)
show()
}
}
viewHolder.layoutView.setOnClickListener(listener)
viewHolder.infoView.setOnClickListener(listener)
viewHolder.textView.setOnClickListener(listener)
val message = rows[position]
val down = message.direction == MESSAGE_DOWN
val lp = viewHolder.layoutView.layoutParams as LinearLayout.LayoutParams
val peer: String = if (down) {
lp.setMargins(0, 10, 75, 10)
viewHolder.layoutView.setBackgroundResource(R.drawable.message_in_bg)
if (chatPeer.startsWith("sip:") &&
(Utils.uriHostPart(message.peerUri) == Utils.uriHostPart(message.aor)))
Utils.uriUserPart(message.peerUri)
else
chatPeer
} else {
lp.setMargins(75, 10, 0, 10)
viewHolder.layoutView.setBackgroundResource(R.drawable.message_out_bg)
ctx.getString(R.string.you)
}
viewHolder.layoutView.layoutParams = lp
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 = "${ctx.getString(R.string.today)} $info"
info = "$info - $peer"
if (message.direction == MESSAGE_UP_FAIL) {
info = if (message.responseCode != 0)
"$info - ${ctx.getString(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
else
"$info - ${ctx.getString(R.string.sending_failed)}"
viewHolder.infoView.setTextColor(ContextCompat.getColor(ctx, R.color.colorAccent))
}
viewHolder.infoView.text = info
viewHolder.textView.text = message.message
if (message.new)
viewHolder.textView.setTypeface(null, Typeface.BOLD)
return rowView
}
}

View File

@ -1,79 +0,0 @@
package com.tutpro.baresip
import android.annotation.SuppressLint
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import kotlin.math.abs
open class OnSwipeTouchListener(ctx: Context) : View.OnTouchListener {
private val gestureDetector: GestureDetector
companion object {
private const val SWIPE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100
}
init {
gestureDetector = GestureDetector(ctx, GestureListener())
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View?, event: MotionEvent): Boolean {
return try {
gestureDetector.onTouchEvent(event)
} catch (e: Exception) {
Log.e(TAG, "onTouch exception $e")
e.printStackTrace()
false
}
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(event: MotionEvent): Boolean {
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velX: Float, velY: Float): Boolean {
var result = false
try {
val diffY = e2.y - e1!!.y
val diffX = e2.x - e1.x
if (abs(diffX) > abs(diffY)) {
if (abs(diffX) > SWIPE_THRESHOLD && abs(velX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight()
} else {
onSwipeLeft()
}
result = true
}
} else if (abs(diffY) > SWIPE_THRESHOLD && abs(velY) > SWIPE_VELOCITY_THRESHOLD) {
result = if (diffY > 0) {
onSwipeBottom()
false
} else {
onSwipeTop()
true
}
}
} catch (e: Exception) {
Log.e(TAG, "onFling exception $e")
// e.printStackTrace()
}
return result
}
}
open fun onSwipeRight() {}
open fun onSwipeLeft() {}
open fun onSwipeTop() {}
open fun onSwipeBottom() {}
}

View File

@ -1,57 +0,0 @@
package com.tutpro.baresip
import android.content.Context
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
class UaSpinnerAdapter(cxt: Context, private val uas: ArrayList<UserAgent>) :
ArrayAdapter<UserAgent>(cxt, android.R.layout.simple_spinner_item, uas) {
private val layoutInflater = LayoutInflater.from(context)
private class ViewHolder(view: View?) {
val textView: TextView = view?.findViewById(R.id.spinnerText)!!
val imageView: ImageView = view?.findViewById(R.id.spinnerImage)!!
}
override fun getDropDownView(position: Int, view: View?, parent: ViewGroup): View {
return getImageForPosition(position, view, parent)
}
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
return getImageForPosition(position, view, parent)
}
private fun getImageForPosition(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder
val rowView: View
if (view == null) {
rowView = layoutInflater.inflate(R.layout.account_spinner, parent, false)
viewHolder = ViewHolder(rowView)
rowView.tag = viewHolder
} else {
rowView = view
viewHolder = rowView.tag as ViewHolder
}
val ua = uas[position]
viewHolder.textView.text = if (ua.account.nickName != "")
ua.account.nickName
else
ua.account.aor.split(":")[1]
viewHolder.textView.textSize = 17f
if (BaresipService.uas.size > 1 && ua.calls().isNotEmpty())
viewHolder.textView.setTypeface(null, Typeface.BOLD)
viewHolder.imageView.setImageResource(ua.status)
return rowView
}
}

View File

@ -1,5 +1,8 @@
package com.tutpro.baresip
import com.tutpro.baresip.BaresipService.Companion.uas
import com.tutpro.baresip.BaresipService.Companion.uasStatus
class UserAgent(val uap: Long) {
val account = Account(Api.ua_account(uap))
@ -10,11 +13,22 @@ class UserAgent(val uap: Long) {
}
fun add() {
BaresipService.uas.add(this)
val updatedUas = uas.value.toMutableList()
updatedUas.add(this)
uas.value = updatedUas.toList()
uasStatus.value = statusMap()
}
fun remove() {
BaresipService.uas.remove(this)
val updatedUas = uas.value.toMutableList()
updatedUas.remove(this)
uas.value = updatedUas.toList()
uasStatus.value = statusMap()
}
fun uaUpdateStatus(status: Int) {
uas.value.find { it.uap == this.uap }?.status = status
uasStatus.value = statusMap()
}
fun calls(dir: String = ""): ArrayList<Call> {
@ -34,19 +48,19 @@ class UserAgent(val uap: Long) {
companion object {
fun ofAor(aor: String): UserAgent? {
for (ua in BaresipService.uas)
for (ua in uas.value)
if (ua.account.aor == aor) return ua
return null
}
fun ofDomain(domain: String): UserAgent? {
for (ua in BaresipService.uas)
for (ua in uas.value)
if (Utils.aorDomain(ua.account.aor) == domain) return ua
return null
}
fun ofUap(uap: Long): UserAgent? {
for (ua in BaresipService.uas)
for (ua in uas.value)
if (ua.uap == uap) return ua
return null
}
@ -59,20 +73,27 @@ class UserAgent(val uap: Long) {
}
fun findAorIndex(aor: String): Int? {
for (i in BaresipService.uas.indices) {
if (BaresipService.uas[i].account.aor == aor) return i
for (i in uas.value.indices) {
if (uas.value[i].account.aor == aor) return i
}
return null
}
fun register() {
for (ua in BaresipService.uas) {
for (ua in uas.value) {
if (ua.account.regint > 0) {
if (Api.ua_register(ua.uap) != 0)
Log.d(TAG, "Failed to register ${ua.account.aor}")
}
}
}
fun statusMap(): Map<String, Int> {
val result = emptyMap<String, Int>().toMutableMap()
for (ua in uas.value)
result[ua.account.aor] = ua.status
return result
}
}
}

View File

@ -7,44 +7,60 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Bitmap.createScaledBitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.media.AudioAttributes
import android.net.Uri
import android.net.wifi.WifiManager
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateUtils
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.audiofx.AcousticEchoCanceler
import android.os.*
import android.media.audiofx.AutomaticGainControl
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.text.format.DateUtils
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import androidx.core.net.toUri
import androidx.core.text.isDigitsOnly
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import java.io.*
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import java.lang.reflect.Method
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.security.SecureRandom
import java.text.DateFormat
import java.util.*
import java.util.Calendar
import java.util.Enumeration
import java.util.GregorianCalendar
import java.util.Locale
import java.util.Random
import java.util.concurrent.Executor
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@ -54,6 +70,7 @@ 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 {
@ -161,7 +178,7 @@ object Utils {
return u
}
fun e164Uri(uri: String, countryCode: String): String {
private fun e164Uri(uri: String, countryCode: String): String {
if (countryCode == "") return uri
val scheme = uri.substring(0, 4)
val userPart = uriUserPart(uri)
@ -488,30 +505,6 @@ object Utils {
return result
}
fun dtmfWatcher(callp: Long): TextWatcher {
return object : TextWatcher {
override fun beforeTextChanged(sequence: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(sequence: CharSequence, start: Int, before: Int, count: Int) {
val text = sequence.subSequence(start, start + count).toString()
if (text.isNotEmpty()) {
val digit = text[0]
val call = Call.ofCallp(callp)
if (call == null) {
Log.w(TAG, "dtmfWatcher did not find call $callp")
} else {
Log.d(TAG, "Got DTMF digit '$digit'")
if (((digit >= '0') && (digit <= '9')) || (digit == '*') || (digit == '#'))
call.sendDigit(digit)
}
}
}
override fun afterTextChanged(sequence: Editable) {
// KEYCODE_REL
// call_send_digit(callp, 4.toChar())
}
}
}
fun checkPermissions(ctx: Context, permissions: Array<String>) : Boolean {
for (p in permissions) {
if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) {
@ -524,23 +517,6 @@ object Utils {
return true
}
fun View.showSnackBar(
view: View,
msg: String,
length: Int,
actionMessage: CharSequence?,
action: (View) -> Unit
) {
val snackBar = Snackbar.make(view, msg, length)
if (actionMessage != null) {
snackBar.setAction(actionMessage) {
action(this)
}.show()
} else {
snackBar.show()
}
}
fun copyAssetToFile(context: Context, asset: String, path: String) {
try {
val `is` = context.assets.open(asset)
@ -649,7 +625,7 @@ object Utils {
if (file.exists()) file.delete()
try {
val out = FileOutputStream(file)
val scaledBitmap = createScaledBitmap(bitmap, 96, 96, true)
val scaledBitmap = bitmap.scale(96, 96)
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush()
out.close()
@ -663,7 +639,6 @@ object Utils {
class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable {
companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID: Long = -29238082928391L
}
}
@ -862,7 +837,7 @@ object Utils {
fun randomColor(): Int {
val rnd = Random()
return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256),
return android.graphics.Color.argb(255, rnd.nextInt(256), rnd.nextInt(256),
rnd.nextInt(256))
}
@ -877,6 +852,19 @@ object Utils {
}
fun isThemeDark(ctx: Context) : Boolean {
return Preferences(ctx).displayTheme == AppCompatDelegate.MODE_NIGHT_YES ||
ctx.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
}
@Composable
fun isNightMode() = when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_NO -> false
AppCompatDelegate.MODE_NIGHT_YES -> true
else -> isSystemInDarkTheme()
}
fun relativeTime(ctx: Context, time: GregorianCalendar): String {
return if (DateUtils.isToday(time.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.SHORT)
@ -894,8 +882,8 @@ object Utils {
}
}
fun bitmapFromView(view: View): Bitmap {
val bitmap = Bitmap.createBitmap(view.layoutParams.width, view.layoutParams.height, Bitmap.Config.ARGB_8888)
private fun bitmapFromView(view: View): Bitmap {
val bitmap = createBitmap(view.layoutParams.width, view.layoutParams.height)
val canvas = Canvas(bitmap)
view.layout(0, 0, view.layoutParams.width, view.layoutParams.height)
view.draw(canvas)
@ -1044,32 +1032,41 @@ object Utils {
}
}
fun isDarkTheme(ctx: Context): Boolean {
return ctx.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
fun aecAgcCheck() {
fun isAecSupported(): Boolean {
if (!AcousticEchoCanceler.isAvailable()) {
Log.i(TAG, "Hardware AEC is NOT available")
return false
}
val sessionId = Api.create_AAudio_SessionId()
val sessionId = Api.AAudio_open_stream()
if (sessionId == -1) {
Log.e(TAG, "Failed to create AAudio stream for session ID")
return false
Log.e(TAG, "Failed to open AAudio stream")
return
}
val aec = AcousticEchoCanceler.create(sessionId)
return if (aec != null) {
aec.release()
Log.i(TAG, "Hardware AEC is supported")
true
} else {
Log.w(TAG, "Hardware AEC is NOT supported")
false
if (AcousticEchoCanceler.isAvailable()) {
val aec = AcousticEchoCanceler.create(sessionId)
if (aec != null) {
BaresipService.aecAvailable = true
aec.release()
Log.d(TAG, "Creation of hardware AEC for $sessionId succeeded")
} else {
Log.w(TAG, "Creation of hardware AEC for $sessionId failed")
}
}
else
Log.i(TAG, "Hardware AEC is NOT available")
if (AutomaticGainControl.isAvailable()) {
val agc = AcousticEchoCanceler.create(sessionId)
if (agc != null) {
BaresipService.agcAvailable = true
agc.release()
Log.d(TAG, "Creation of hardware AGC for $sessionId succeeded")
} else {
Log.w(TAG, "Creation of hardware AGC for $sessionId failed")
}
}
else
Log.i(TAG, "Hardware AGC is NOT available")
Api.AAudio_close_stream()
}
}

View File

@ -0,0 +1,17 @@
package com.tutpro.baresip
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class ViewModel(application: Application) : AndroidViewModel(application) {
private val _selectedAor = MutableStateFlow("")
val selectedAor: StateFlow<String> = _selectedAor.asStateFlow()
fun updateSelectedAor(newValue: String) {
_selectedAor.value = newValue
}
}

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/colorSecondaryDark"/>
<corners android:bottomLeftRadius="5dp" android:radius="15dp" />
</shape>
</item>
</layer-list>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/colorPrimaryDark"/>
<corners android:topRightRadius="5dp" android:radius="15dp" />
</shape>
</item>
</layer-list>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape>
<corners android:radius="10dp" />
<solid android:color="@color/colorGray" />
<!--<stroke
android:width="1dp"
android:color="@color/colorSecondaryDark" />-->
</shape>
</item>
<item android:right="12dp">
<bitmap
android:gravity="end"
android:src="@android:drawable/arrow_down_float" />
</item>
</layer-list>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48dp" android:viewportHeight="24" android:viewportWidth="24" android:width="48dp">
<path android:fillColor="#000000" android:pathData="m8.71,11.71l2.59,2.59c0.39,0.39 1.02,0.39 1.41,0l2.59,-2.59c0.63,-0.63 0.18,-1.71 -0.71,-1.71H9.41c-0.89,0 -1.33,1.08 -0.7,1.71"/>
</vector>

View File

@ -4,6 +4,6 @@
android:viewportWidth="960"
android:viewportHeight="960" >
<path
android:fillColor="@color/colorTrafficGreen"
android:fillColor="@color/colorSecondary"
android:pathData="M370,640L430,640L430,320L370,320L370,640ZM530,640L590,640L590,320L530,320L530,640ZM480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 111.5,324Q143,251 197.5,197Q252,143 325,111.5Q398,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,562 848.5,635Q817,708 763,762.5Q709,817 636,848.5Q563,880 480,880ZM480,820Q622,820 721,720.5Q820,621 820,480Q820,338 721,239Q622,140 480,140Q339,140 239.5,239Q140,338 140,480Q140,621 239.5,720.5Q339,820 480,820ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@ -4,6 +4,6 @@
android:viewportWidth="960"
android:viewportHeight="960" >
<path
android:fillColor="@color/colorTrafficGreen"
android:fillColor="@color/colorSecondary"
android:pathData="M480,626L626,480L480,334L438,376L512,450L330,450L330,510L512,510L438,584L480,626ZM480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 111.5,324Q143,251 197.5,197Q252,143 325,111.5Q398,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,562 848.5,635Q817,708 763,762.5Q709,817 636,848.5Q563,880 480,880ZM480,820Q622,820 721,720.5Q820,621 820,480Q820,338 721,239Q622,140 480,140Q339,140 239.5,239Q140,338 140,480Q140,621 239.5,720.5Q339,820 480,820ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@ -1,6 +1,5 @@
<vector android:height="48dp" android:tint="@color/colorAccent"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
<path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</vector>

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/accountRow"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/aor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/action"
android:scrollHorizontally="true"
android:textSize="20sp"
android:layout_marginBottom="6dp">
</TextView>
<ImageButton
android:id="@+id/action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_marginStart="6dp"
android:icon="@android:drawable/ic_menu_delete"
android:background="@null"
android:scaleType="centerCrop"
android:contentDescription="@string/delete">
</ImageButton>
</RelativeLayout>

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="UselessParent"
android:padding="11dp" >
<ImageView
android:id="@+id/spinnerImage"
android:layout_marginStart="5dp"
android:contentDescription="@string/status" />
<TextView
android:id="@+id/spinnerText"
android:textColor="@color/colorSpinnerText"
android:paddingStart="8dp"
android:paddingEnd="0dp" />
</TableRow>
</TableLayout>

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AboutActivity" >
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
tools:ignore="UselessParent"
android:fillViewport="true" >
<TextView
android:id="@+id/aboutText"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>

View File

@ -1,510 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/AccountView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
android:focusableInTouchMode="true" >
<TextView
android:id="@+id/Uri"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="18sp"
android:paddingBottom="10dp"
android:text="" >
</TextView>
<TextView
android:id="@+id/NickNameTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="@string/account_nickname" >
</TextView>
<EditText
android:id="@+id/NickName"
android:layout_height="wrap_content"
android:hint="@string/nickname"
android:textColorHint="@color/colorSecondaryDark"
android:inputType="textPersonName|textCapWords"
android:textSize="18sp"
android:autofillHints="name"
android:layout_width="fill_parent">
</EditText>
<TextView
android:id="@+id/DisplayNameTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/display_name" >
</TextView>
<EditText
android:id="@+id/DisplayName"
android:layout_height="wrap_content"
android:hint="@string/your_name"
android:textColorHint="@color/colorSecondaryDark"
android:inputType="textPersonName|textCapWords"
android:textSize="18sp"
android:autofillHints="name"
android:layout_width="fill_parent">
</EditText>
<TextView
android:id="@+id/AuthUserTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/authentication_username" >
</TextView>
<EditText
android:id="@+id/AuthUser"
android:layout_height="wrap_content"
android:hint="@string/user_id"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="18sp"
android:inputType="textEmailAddress"
android:autofillHints="username"
android:layout_width="fill_parent">
</EditText>
<TextView
android:id="@+id/AuthPassTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/authentication_password" >
</TextView>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintEnabled="false"
app:endIconMode="password_toggle">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/AuthPass"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="flagNoExtractUi"
android:inputType="textPassword"
android:maxLines="1">
</androidx.appcompat.widget.AppCompatEditText>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/OutboundProxyTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:textSize="18sp"
android:text="@string/outbound_proxies" >
</TextView>
<EditText
android:id="@+id/Outbound1"
android:layout_height="wrap_content"
android:hint="@string/sip_uri_of_proxy_server"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="18sp"
android:inputType="textEmailAddress"
android:importantForAutofill="no"
android:layout_width="fill_parent" >
</EditText>
<EditText
android:id="@+id/Outbound2"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/sip_uri_of_another_proxy_server"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="18sp"
android:inputType="textEmailAddress"
android:importantForAutofill="no" >
</EditText>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/RegTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Register"
android:textSize="18sp"
android:text="@string/register" >
</TextView>
<CheckBox
android:id="@+id/Register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:layout_centerVertical="true"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/RegIntTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/RegInt"
android:textSize="18sp"
android:text="@string/reg_int" >
</TextView>
<EditText
android:id="@+id/RegInt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:layout_centerVertical="true"
android:textSize="18sp"
android:inputType="number"
android:importantForAutofill="no" >
</EditText>
</RelativeLayout>
<TextView
android:id="@+id/AudioCodecsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:textStyle="bold"
android:text="@string/audio_codecs" >
</TextView>
<TextView
android:id="@+id/MediaEncTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:textSize="18sp"
android:text="@string/media_encryption" >
</TextView>
<Spinner
android:id="@+id/mediaEncSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp" >
</Spinner>
<TextView
android:id="@+id/MediaNatTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:textSize="18sp"
android:text="@string/media_nat" >
</TextView>
<Spinner
android:id="@+id/mediaNatSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp" >
</Spinner>
<LinearLayout
android:id="@+id/Stun"
android:layout_width="fill_parent"
android:orientation="vertical"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/StunServerTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/stun_server" >
</TextView>
<EditText
android:id="@+id/StunServer"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize="18sp"
android:scrollHorizontally="true"
android:importantForAutofill="no"
android:hint="@string/stun_server_uri"
android:textColorHint="@color/colorSecondaryDark"
android:inputType="textEmailAddress" >
</EditText>
<TextView
android:id="@+id/StunUserTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/stun_username" >
</TextView>
<EditText
android:id="@+id/StunUser"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/user_id"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="18sp"
android:inputType="textEmailAddress"
android:autofillHints="username" >
</EditText>
<TextView
android:id="@+id/StunPassTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/stun_password" >
</TextView>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
app:hintEnabled="false"
app:endIconMode="password_toggle">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/StunPass"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="flagNoExtractUi"
android:inputType="textPassword"
android:hint="@string/password"
android:textColorHint="@color/colorSecondaryDark"
android:maxLines="1">
</androidx.appcompat.widget.AppCompatEditText>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/RtcpMuxTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/RtcpMux"
android:textSize="18sp"
android:text="@string/rtcp_mux" >
</TextView>
<CheckBox
android:id="@+id/RtcpMux"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:layout_centerVertical="true"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="0dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/Rel100Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Rel100"
android:textSize="18sp"
android:text="@string/rel_100" >
</TextView>
<CheckBox
android:id="@+id/Rel100"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:layout_centerVertical="true"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<TextView
android:id="@+id/DtmfModeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:textSize="18sp"
android:text="@string/dtmf_mode" >
</TextView>
<Spinner
android:id="@+id/dtmfModeSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp" >
</Spinner>
<TextView
android:id="@+id/AnswerModeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="14dp"
android:textSize="18sp"
android:text="@string/answer_mode" >
</TextView>
<Spinner
android:id="@+id/answerModeSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp" >
</Spinner>
<TextView
android:id="@+id/RedirectModeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="14dp"
android:textSize="18sp"
android:text="@string/redirect_mode" >
</TextView>
<Spinner
android:id="@+id/redirectModeSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp" >
</Spinner>
<TextView
android:id="@+id/VoicemailUriTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="14dp"
android:textSize="18sp"
android:text="@string/voicemail_uri" >
</TextView>
<EditText
android:id="@+id/voicemailUri"
android:layout_height="wrap_content"
android:hint="@string/sip_uri"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="18sp"
android:inputType="textUri"
android:importantForAutofill="no"
android:layout_width="fill_parent">
</EditText>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/CountryCodeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/countryCode"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/country_code" >
</TextView>
<EditText
android:id="@+id/countryCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:layout_centerVertical="true"
android:hint="@string/country_code_hint"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="18sp"
android:inputType="phone"
android:importantForAutofill="no" >
</EditText>
</RelativeLayout>
<TextView
android:id="@+id/TelephonyProviderTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textSize="18sp"
android:text="@string/telephony_provider" >
</TextView>
<EditText
android:id="@+id/telephonyProvider"
android:layout_height="wrap_content"
android:hint="@string/telephony_provider_hint"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="18sp"
android:inputType="textUri"
android:importantForAutofill="no"
android:layout_width="fill_parent">
</EditText>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp">
<TextView
android:id="@+id/DefaultTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Default"
android:textSize="18sp"
android:text="@string/default_account" >
</TextView>
<CheckBox
android:id="@+id/Default"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
</LinearLayout>
</ScrollView>

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/AccountsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.tutpro.baresip.AccountsActivity" >
<ListView
android:id="@+id/accounts"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_above="@id/newAor"
android:transcriptMode="alwaysScroll" >
</ListView>
<EditText
android:id="@+id/newAor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textSize="20sp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="0dp"
android:layout_toStartOf="@+id/addAccount"
android:inputType="textEmailAddress"
android:autofillHints="username"
android:scrollHorizontally="true"
android:hint="@string/user_domain"
android:textColorHint="@color/colorSecondaryDark" >
</EditText>
<ImageButton
android:id="@+id/addAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_gravity="end"
android:layout_marginStart="6dp"
android:src="@drawable/plus"
android:background="@null"
android:contentDescription="@string/add"
android:scaleType="centerCrop" >
</ImageButton>
</RelativeLayout>

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ContactView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
android:orientation="vertical">
<TextView
android:id="@+id/TextAvatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_horizontal"
android:scaleType="centerInside"
android:background="@drawable/circle"
android:textSize="72sp"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:gravity="center" >
</TextView>
<androidx.cardview.widget.CardView
android:id="@+id/CardAvatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_horizontal"
android:elevation="12dp"
app:cardCornerRadius="48dp" >
<ImageView
android:id="@+id/ImageAvatar"
android:contentDescription="@string/avatar_image"
android:layout_height="96dp"
android:layout_width="96dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" >
</ImageView>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/Name"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_marginTop="24dp"
android:layout_marginBottom="12dp"
android:textColor="@color/colorStrong"
android:textSize="24sp"
tools:ignore="LabelFor">
</TextView>
<ListView
android:id="@+id/uris"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:divider="#00000000"
android:dividerHeight="0dp" >
</ListView>
</LinearLayout>

View File

@ -1,264 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/AudioView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:focusableInTouchMode="true" >
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/VolumeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/VolumeSpinner"
android:textSize="18sp"
android:text="@string/default_call_volume" >
</TextView>
<Spinner
android:id="@+id/VolumeSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end" >
</Spinner>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp" >
<TextView
android:id="@+id/SpeakerPhoneTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/SpeakerPhone"
android:textSize="18sp"
android:text="@string/speaker_phone" >
</TextView>
<CheckBox
android:id="@+id/SpeakerPhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="vertical" >
<TextView
android:id="@+id/AudioModulesTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="@string/audio_modules_title" >
</TextView>
<LinearLayout
android:id="@+id/AudioModulesList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="24dp"
android:orientation="vertical" >
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/OpusBitRateTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="18sp"
android:text="@string/opus_bit_rate" >
</TextView>
<EditText
android:id="@+id/OpusBitRate"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/OpusBitRateTitle"
android:layout_marginStart="10dp"
android:textSize="18sp"
android:scrollHorizontally="true"
android:inputType="numberDecimal"
android:hint="@string/_28000"
android:textColorHint="@color/colorSecondaryDark"
android:importantForAutofill="no" >
</EditText>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/OpusPacketLossTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="18sp"
android:text="@string/opus_packet_loss" >
</TextView>
<EditText
android:id="@+id/OpusPacketLoss"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/OpusPacketLossTitle"
android:layout_marginStart="10dp"
android:textSize="18sp"
android:scrollHorizontally="true"
android:inputType="numberDecimal"
android:hint="@string/_0"
android:textColorHint="@color/colorSecondaryDark"
android:importantForAutofill="no" >
</EditText>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/AudioDelayTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="18sp"
android:text="@string/audio_delay" >
</TextView>
<EditText
android:id="@+id/AudioDelay"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/AudioDelayTitle"
android:layout_marginStart="10dp"
android:textSize="18sp"
android:scrollHorizontally="true"
android:inputType="number"
android:hint="@string/_0"
android:textColorHint="@color/colorSecondaryDark"
android:importantForAutofill="no" >
</EditText>
</RelativeLayout>
<RelativeLayout
android:id="@+id/AecLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" >
<TextView
android:id="@+id/AecTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Aec"
android:textSize="18sp"
android:text="@string/aec" >
</TextView>
<CheckBox
android:id="@+id/Aec"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:id="@+id/MicGainLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/MicGainTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="18sp"
android:text="@string/microphone_gain" >
</TextView>
<EditText
android:id="@+id/MicGain"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/MicGainTitle"
android:layout_marginStart="10dp"
android:textSize="18sp"
android:scrollHorizontally="true"
android:inputType="numberDecimal"
android:hint="@string/_1.0"
android:textColorHint="@color/colorSecondaryDark"
android:importantForAutofill="no" >
</EditText>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="0dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/ToneCountryTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/ToneCountrySpinner"
android:textSize="18sp"
android:text="@string/tone_country" >
</TextView>
<Spinner
android:id="@+id/ToneCountrySpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end" >
</Spinner>
</RelativeLayout>
</LinearLayout>
</ScrollView>

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/CallDetailsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".CallDetailsActivity">
<TextView
android:id="@+id/peer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textSize="16sp"
android:gravity="center_horizontal" >
</TextView>
<RelativeLayout
android:id="@+id/headings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer"
android:orientation="horizontal" >
<TextView android:id="@+id/direction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:textSize="16sp"
android:text="@string/direction" >
</TextView>
<TextView android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginStart="76dp"
android:textSize="16sp"
android:text="@string/time" >
</TextView>
<TextView android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:textSize="16sp"
android:text="@string/calls_duration">
</TextView>
</RelativeLayout>
<ListView
android:id="@+id/calls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/headings"
android:layout_marginTop="12dp"
android:divider="#00000000"
android:dividerHeight="10dp" >
</ListView>
</RelativeLayout>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/CallsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".CallsActivity">
<TextView
android:id="@+id/account"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textSize="16sp"
android:gravity="center_horizontal" >
</TextView>
<ListView
android:id="@+id/calls"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_below="@id/account"
android:divider="#00000000"
android:dividerHeight="10dp" >
</ListView>
</RelativeLayout>

View File

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ChatView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".ChatActivity" >
<TextView
android:id="@+id/account"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center_horizontal" >
</TextView>
<ListView
android:id="@+id/messages"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_below="@id/account"
android:transcriptMode="alwaysScroll"
android:stackFromBottom="true"
android:divider="#00000000"
android:dividerHeight="10dp"
android:layout_above="@id/text" >
</ListView>
<EditText
android:id="@+id/text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@id/sendButton"
android:textSize="18sp"
android:inputType="textMultiLine|textShortMessage|textCapSentences|textAutoComplete|textAutoCorrect"
android:scrollHorizontally="false"
android:autofillHints="text"
android:hint="@string/new_message"
android:textColorHint="@color/colorSecondaryDark" >
</EditText>
<ImageButton
android:id="@+id/sendButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="2dp"
android:paddingTop="10dp"
android:src="@drawable/send"
android:background="@null"
android:contentDescription="@string/send" >
</ImageButton>
</RelativeLayout>

View File

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ChatsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".ChatsActivity" >
<TextView
android:id="@+id/account"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center_horizontal" >
</TextView>
<ListView
android:id="@+id/chats"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_below="@+id/account"
android:layout_above="@id/peer"
android:layout_marginBottom="10dp"
android:transcriptMode="alwaysScroll"
android:stackFromBottom="true"
android:divider="#00000000"
android:dividerHeight="16dp" >
</ListView>
<AutoCompleteTextView
android:id="@+id/peer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@+id/plusButton"
android:textSize="18sp"
android:inputType="textEmailAddress"
android:hint="@string/new_chat_peer"
android:textColorHint="@color/colorSecondaryDark" >
</AutoCompleteTextView>
<ImageButton
android:id="@+id/plusButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="2dp"
android:src="@drawable/plus"
android:background="@null"
android:contentDescription="@string/add" >
</ImageButton>
</RelativeLayout>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/CodecsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".CodecsActivity" >
<TextView
android:id="@+id/CodecsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@string/audio_codecs" >
</TextView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/CodecList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:scrollbars="vertical" />
</RelativeLayout>

View File

@ -1,384 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ConfigView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:focusableInTouchMode="true" >
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/AutoStartTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/AutoStart"
android:textSize="18sp"
android:text="@string/start_automatically" >
</TextView>
<CheckBox
android:id="@+id/AutoStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<TextView
android:id="@+id/ListenAddressTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/listen_address"
android:textSize="18sp" >
</TextView>
<EditText
android:id="@+id/ListenAddress"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-10dp"
android:layout_marginBottom="12dp"
android:textSize="18sp"
android:hint="@string/_0_0_0_0_5060"
android:textColorHint="@color/colorSecondaryDark"
android:inputType="textUri"
android:importantForAutofill="no" >
</EditText>
<TextView
android:id="@+id/NetAfTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="@string/address_family" >
</TextView>
<Spinner
android:id="@+id/NetAfSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:paddingTop="8dp" >
</Spinner>
<TextView
android:id="@+id/DnsServersTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dns_servers"
android:textSize="18sp">
</TextView>
<EditText
android:id="@+id/DnsServers"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-10dp"
android:layout_marginBottom="12dp"
android:textSize="18sp"
android:inputType="textUri"
android:importantForAutofill="no" >
</EditText>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/CertificateFileTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/CertificateFile"
android:textSize="18sp"
android:text="@string/tls_certificate_file" >
</TextView>
<CheckBox
android:id="@+id/CertificateFile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/VerifyServerTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/VerifyServer"
android:textSize="18sp"
android:text="@string/verify_server" >
</TextView>
<CheckBox
android:id="@+id/VerifyServer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/CAFileTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/CAFile"
android:textSize="18sp"
android:text="@string/tls_ca_file" >
</TextView>
<CheckBox
android:id="@+id/CAFile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<TextView
android:id="@+id/UserAgentTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/user_agent"
android:textSize="18sp" >
</TextView>
<EditText
android:id="@+id/UserAgent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-10dp"
android:layout_marginBottom="12dp"
android:textSize="18sp"
android:inputType="text"
android:importantForAutofill="no" >
</EditText>
<TextView
android:id="@+id/AudioSettingsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="18dp"
android:textSize="18sp"
android:textStyle="bold"
android:text="@string/audio_settings" >
</TextView>
<TextView
android:id="@+id/ContactsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="@string/contacts" >
</TextView>
<Spinner
android:id="@+id/contactsSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:paddingTop="8dp" >
</Spinner>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/DarkThemeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/DarkTheme"
android:textSize="18sp"
android:text="@string/dark_theme" >
</TextView>
<CheckBox
android:id="@+id/DarkTheme"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:id="@+id/Battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/BatteryOptimizationsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/BatteryOptimizations"
android:textSize="18sp"
android:text="@string/battery_optimizations" >
</TextView>
<CheckBox
android:id="@+id/BatteryOptimizations"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:id="@+id/PhoneApp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/DefaultPhoneAppTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/DefaultPhoneApp"
android:textSize="18sp"
android:text="@string/default_phone_app" >
</TextView>
<CheckBox
android:id="@+id/DefaultPhoneApp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/DebugTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Debug"
android:textSize="18sp"
android:text="@string/debug" >
</TextView>
<CheckBox
android:id="@+id/Debug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/SipTraceTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/SipTrace"
android:textSize="18sp"
android:text="@string/sip_trace" >
</TextView>
<CheckBox
android:id="@+id/SipTrace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/ResetTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Reset"
android:textSize="18sp"
android:text="@string/reset_config" >
</TextView>
<CheckBox
android:id="@+id/Reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
</LinearLayout>
</ScrollView>

View File

@ -1,139 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ContactView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
android:orientation="vertical">
<TextView
android:id="@+id/TextAvatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_horizontal"
android:scaleType="centerInside"
android:background="@drawable/circle"
android:textSize="72sp"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:gravity="center" >
</TextView>
<androidx.cardview.widget.CardView
android:id="@+id/CardAvatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_horizontal"
android:elevation="12dp"
app:cardCornerRadius="48dp" >
<ImageView
android:id="@+id/ImageAvatar"
android:contentDescription="@string/avatar_image"
android:layout_height="96dp"
android:layout_width="96dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" >
</ImageView>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/NameTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:paddingTop="10dp"
android:text="@string/contact_name" >
</TextView>
<EditText
android:id="@+id/Name"
android:layout_height="wrap_content"
android:inputType="textPersonName|textCapWords"
android:textSize="18sp"
android:layout_width="fill_parent"
android:importantForAutofill="no"
tools:ignore="LabelFor">
</EditText>
<TextView
android:id="@+id/SipOrTelUriTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:paddingTop="10dp"
android:text="@string/sip_or_tel_uri" >
</TextView>
<EditText
android:id="@+id/Uri"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="@string/user_domain_or_number"
android:textColorHint="@color/colorSecondaryDark"
android:inputType="textEmailAddress"
android:textSize="18sp"
android:importantForAutofill="no"
tools:ignore="TextFields" >
</EditText>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/FavoriteTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Favorite"
android:textSize="18sp"
android:text="@string/favorite" >
</TextView>
<CheckBox
android:id="@+id/Favorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/AndroidTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/Android"
android:textSize="18sp"
android:text="@string/android" >
</TextView>
<CheckBox
android:id="@+id/Android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="end"
android:checked="false" >
</CheckBox>
</RelativeLayout>
</LinearLayout>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ContactsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".ContactsActivity">
<ListView
android:id="@+id/contacts"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_above="@id/plusButton"
android:divider="#00000000"
android:dividerHeight="10dp" >
</ListView>
<ImageButton
android:id="@+id/plusButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/plus"
android:background="@null"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:contentDescription="@string/add" >
</ImageButton>
</RelativeLayout>

View File

@ -1,403 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:theme="@style/ActionBar">
</androidx.appcompat.widget.Toolbar>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:layout_below="@id/toolbar" >
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="@+id/mainActivityLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Spinner
android:id="@+id/aorSpinner"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/spinner_bg"
android:popupBackground="@color/colorSpinnerDropdown"
android:gravity="center_vertical"
android:spinnerMode="dropdown"/>
<LinearLayout
android:id="@+id/callTitleRow"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/aorSpinner"
android:layout_marginTop="10dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/callTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:gravity="start"
android:text="@string/outgoing_call_to_dots"
android:textSize="20sp">
</TextView>
<Chronometer
android:id="@+id/callTimer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:paddingEnd="3dp"
android:gravity="end"
android:visibility="invisible"
android:textSize="18sp">
</Chronometer>
</LinearLayout>
<RelativeLayout
android:id="@+id/uriRow"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/callTitleRow"
android:layout_marginTop="14dp"
android:layout_marginStart="2dp"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="32dp"
app:endIconMode="clear_text">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/callUri"
android:layout_width="fill_parent"
android:layout_height="32dp"
android:completionThreshold="3"
android:inputType="textEmailAddress"
android:textColorHint="@color/colorSecondaryDark"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/securityButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="0dp"
android:layout_marginTop="4dp"
android:layout_alignParentEnd="true"
android:layout_gravity="center_vertical"
android:visibility="invisible"
android:background="#00000000"
android:contentDescription="@string/app_name" >
</ImageButton>
</RelativeLayout>
<LinearLayout
android:id="@+id/diverter"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/uriRow"
android:visibility="gone"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:gravity="start"
android:text="@string/diverted_by_dots"
android:textSize="20sp">
</TextView>
<TextView
android:id="@+id/diverterUri"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:paddingTop="10dp"
android:paddingStart="6dp"
android:paddingEnd="24dp"
android:textColor="@color/colorStrong"
android:textSize="20sp"
android:layout_weight="1" >
</TextView>
<View
android:layout_width="fill_parent"
android:layout_height="1.5dp"
android:layout_marginStart="3dp"
android:layout_marginEnd="28dp"
android:background="@color/colorGray"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/callRow"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/diverter"
android:paddingTop="12dp"
android:paddingBottom="10dp" >
<ImageButton
android:id="@+id/callButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:background="@null"
android:src="@drawable/call"
android:contentDescription="@string/call" >
</ImageButton>
<ImageButton
android:id="@+id/hangupButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:background="@null"
android:src="@drawable/hangup"
android:clickable="false"
android:visibility="invisible"
android:contentDescription="@string/hangup" >
</ImageButton>
<ImageButton
android:id="@+id/answerButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dp"
android:background="@null"
android:src="@drawable/call"
android:clickable="false"
android:visibility="invisible"
android:contentDescription="@string/answer" >
</ImageButton>
<ImageButton
android:id="@+id/rejectButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:background="@null"
android:src="@drawable/hangup"
android:clickable="false"
android:visibility="invisible"
android:contentDescription="@string/reject" >
</ImageButton>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="70dp"
android:orientation="horizontal"
android:visibility="invisible"
android:id="@+id/callControl" >
<ImageButton
android:id="@+id/holdButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:src="@drawable/call_hold"
android:background="@null"
android:clickable="false"
android:contentDescription="@string/hold" >
</ImageButton>
<ImageButton
android:id="@+id/transferButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:src="@drawable/call_transfer"
android:background="@null"
android:layout_marginStart="60dp"
android:clickable="false"
android:contentDescription="@string/call_transfer" >
</ImageButton>
<EditText
android:id="@+id/dtmf"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="120dp"
android:layout_marginEnd="48dp"
android:singleLine="true"
android:scrollHorizontally="true"
android:hint="@string/dtmf"
android:textColorHint="@color/colorSecondaryDark"
android:importantForAutofill="no"
android:inputType="phone"
android:digits="0123456789#*"
android:textSize="20sp" >
</EditText>
<ImageButton
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:layout_marginEnd="0dp"
android:layout_alignParentEnd="true"
android:src="@drawable/info"
android:layout_marginTop="6dp"
android:background="@null"
android:clickable="false"
android:contentDescription="@string/call_info" >
</ImageButton>
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</ScrollView>
<TextView
android:id="@+id/onHoldNotice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/buttons"
android:gravity="center"
android:visibility="gone"
android:textSize="18sp"
android:textColor="@color/colorAccent"
android:text="@string/call_is_on_hold" >
</TextView>
<LinearLayout
android:id="@+id/buttons"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:paddingBottom="6dp"
android:layout_alignParentBottom="true" >
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" >
</Space>
<ImageButton
android:id="@+id/voicemailButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center_horizontal|center_vertical"
android:src="@drawable/voicemail"
android:visibility="gone"
android:contentDescription="@string/voicemail" >
</ImageButton>
<Space
android:id="@+id/voicemailButtonSpace"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:visibility="gone" >
</Space>
<ImageButton
android:id="@+id/contactsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center_horizontal|center_vertical"
android:src="@drawable/contacts"
android:contentDescription="@string/contacts" >
</ImageButton>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" >
</Space>
<ImageButton
android:id="@+id/messagesButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center_horizontal|center_vertical"
android:src="@drawable/messages"
android:contentDescription="@string/messages" >
</ImageButton>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" >
</Space>
<ImageButton
android:id="@+id/callsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center_horizontal|center_vertical"
android:src="@drawable/calls"
android:contentDescription="@string/call_history" >
</ImageButton>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" >
</Space>
<ImageButton
android:id="@+id/dialpadButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center_horizontal|center_vertical"
android:src="@drawable/dialpad_off"
android:contentDescription="@string/dialpad" >
</ImageButton>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" >
</Space>
</LinearLayout>
</RelativeLayout>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="?dialogPreferredPadding"
android:textSize="20sp"
android:background="@color/colorPrimary"
android:textColor="@color/colorWhite" >
</TextView>

View File

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/uriRow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/uri"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:textSize="18sp"
android:textIsSelectable="true"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp" >
</TextView>
<ImageButton
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_marginEnd="48dp"
android:layout_marginStart="10dp"
android:src="@drawable/message"
android:background="@null"
android:scaleType="centerCrop"
android:contentDescription="@string/call" >
</ImageButton>
<ImageButton
android:id="@+id/call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:src="@drawable/call_small"
android:background="@null"
android:scaleType="centerCrop"
android:contentDescription="@string/call" >
</ImageButton>
</RelativeLayout>

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<ImageView
android:id="@+id/direction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginStart="12dp"
android:contentDescription="@string/direction" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginStart="76dp"
android:text="" />
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="12dp"
android:text="" />
</RelativeLayout>

View File

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/TextAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:scaleType="centerInside"
android:background="@drawable/circle"
android:textSize="24sp"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:gravity="center"
android:visibility="gone" >
</TextView>
<androidx.cardview.widget.CardView
android:id="@+id/CardAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:elevation="12dp"
app:cardCornerRadius="18dp" >
<ImageView
android:id="@+id/ImageAvatar"
android:contentDescription="@string/avatar_image"
android:layout_height="36dp"
android:layout_width="36dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" >
</ImageView>
</androidx.cardview.widget.CardView>
<LinearLayout
android:id="@+id/directions"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="41dp"
android:layout_centerVertical="true"
android:paddingBottom="5dp" >
</LinearLayout>
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:focusable="false"
android:gravity="end"
android:textAlignment="gravity"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:paddingStart="0dp"
android:paddingEnd="6dp"
android:textSize="12sp"
android:lines="2"
android:maxLines="2"
android:text="" >
</TextView>
<TextView
android:id="@+id/etc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/directions"
android:paddingTop="10dp"
android:text="" >
</TextView>
<TextView
android:id="@+id/peer_uri"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/etc"
android:layout_toStartOf="@id/time"
android:focusable="false"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textAlignment="gravity"
android:textSize="18sp"
android:maxLines="1"
android:ellipsize="end"
android:text="" >
</TextView>
</RelativeLayout>

View File

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="?dialogPreferredPadding"
android:paddingStart="?dialogPreferredPadding"
android:paddingEnd="?dialogPreferredPadding"
android:paddingBottom="8dp"
android:textSize="20sp"
android:text="@string/call_transfer"
android:textColor="@color/colorPrimary" />
<RelativeLayout
android:id="@+id/blindOrAttended"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="?dialogPreferredPadding"
android:paddingEnd="?dialogPreferredPadding"
android:visibility="gone"
android:orientation="horizontal" >
<TextView
android:id="@+id/blindTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="16sp"
android:text="@string/blind" >
</TextView>
<CheckBox
android:id="@+id/blind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/blindTitle"
android:checked="true" >
</CheckBox>
<TextView
android:id="@+id/attendedTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/attended"
android:textSize="16sp"
android:text="@string/attended" >
</TextView>
<CheckBox
android:id="@+id/attended"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<TextView
android:id="@+id/transferDestination"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="?dialogPreferredPadding"
android:paddingStart="?dialogPreferredPadding"
android:paddingBottom="12dp"
android:textSize="16sp"
android:text="@string/transfer_destination"
android:textColor="@color/colorStrong" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?dialogPreferredPadding"
android:paddingEnd="?dialogPreferredPadding" >
<AutoCompleteTextView
android:id="@+id/transferUri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:textSize="18sp"
android:imeOptions="actionDone"
android:labelFor="@id/transferUri"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/TextAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginTop="10dp"
android:scaleType="centerInside"
android:background="@drawable/circle"
android:textSize="24sp"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:gravity="center"
android:visibility="gone" >
</TextView>
<androidx.cardview.widget.CardView
android:id="@+id/CardAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginTop="10dp"
android:layout_gravity="center_vertical"
android:elevation="12dp"
app:cardCornerRadius="18dp" >
<ImageView
android:id="@+id/ImageAvatar"
android:contentDescription="@string/avatar_image"
android:layout_height="36dp"
android:layout_width="36dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" >
</ImageView>
</androidx.cardview.widget.CardView>
<LinearLayout
android:id="@+id/chat"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="42dp" >
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/peer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
android:textSize="18sp"
android:text="" >
</TextView>
<TextView
android:id="@+id/info"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_gravity="center"
android:gravity="end"
android:textSize="12sp"
android:text="" >
</TextView>
</LinearLayout>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:textSize="16sp"
android:singleLine="true"
android:text="" >
</TextView>
</LinearLayout>
</RelativeLayout>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp" >
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_margin="4dp" >
<TextView
android:id="@+id/codecName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorCodec"
android:textSize="18sp"
android:padding="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/reorderImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/reorder"
android:contentDescription="@string/codec_action" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/contactRow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/TextAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:scaleType="centerInside"
android:background="@drawable/circle"
android:textSize="24sp"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:gravity="center"
android:visibility="gone" >
</TextView>
<androidx.cardview.widget.CardView
android:id="@+id/CardAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:elevation="12dp"
app:cardCornerRadius="18dp" >
<ImageView
android:id="@+id/ImageAvatar"
android:layout_height="36dp"
android:layout_width="36dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@string/avatar_image">
</ImageView>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/contactName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="41dp"
android:textSize="20sp"
android:layout_marginBottom="6dp">
</TextView>
<ImageButton
android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_marginStart="10dp"
android:src="@drawable/edit"
android:background="@null"
android:scaleType="centerCrop"
android:contentDescription="@string/edit" >
</ImageButton>
</RelativeLayout>

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/message"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
tools:ignore="UselessParent" >
<TextView
android:id="@+id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
android:textSize="12sp"
android:text="" >
</TextView>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:textSize="16sp"
android:textIsSelectable="true"
android:text="" >
</TextView>
</LinearLayout>
</LinearLayout>

View File

@ -131,7 +131,11 @@
<string name="start_automatically">Стартирайте автоматично</string>
<string name="start_automatically_help">Ако е отметнато, baresip се стартира автоматично след включване на устройството.</string>
<string name="listen_address">Слушане на адрес</string>
<string name="listen_address_help">IP адрес и порт на формата \'адрес: порт\', на който baresip слуша за входящи SIP заявки. Ако IP адресът е IPv6 адрес, той трябва да бъде написан вътре в скоби []. IPv4 адрес 0.0.0.0 или IPv6 адрес [::] позволява на baresip да слуша всички налични адреси. Ако се остави празно (фабрично по подразбиране), baresip ще слуша на порт 5060 от всички налични адреси.</string>
<string name="listen_address_help">IP адрес и порт на формата \'адрес: порт\', на който baresip слуша
за входящи SIP заявки. Ако IP адресът е IPv6 адрес, той трябва да бъде написан вътре в скоби [].
IPv4 адрес 0.0.0.0 или IPv6 адрес [::] позволява на baresip да слуша всички налични адреси.
Ако се остави празно (фабрично по подразбиране), baresip ще слуша на порт 5060 от всички налични адреси.
</string>
<string name="invalid_listen_address">Невалиден адрес за слушане</string>
<string name="dns_servers">DNS сървъри</string>
<string name="dns_servers_help">Списък разделен със запетая на адреси на DNS сървъри. Ако не е зададено,
@ -147,7 +151,6 @@
<string name="tls_ca_file">TLS CA File</string>
<string name="tls_ca_file_help">Ако е поставено отметка, файл \'ca_certs.crt\' съдържащи TLS сертификати на Сертифициращите органи е бил или ще бъде зареден от директорията за Изтегляне.</string>
<string name="aec">Акустично отменяне на ехото</string>
<string name="aec_help">Ако е поставена отметка, се прави опит за отмяна на ехо при аудио повикване.</string>
<string name="opus_bit_rate">Opus Bit Rate</string>
<string name="opus_bit_rate_help">Средна максимална битова скорост, използвана от аудио поток на Opus.
Валидните стойности са 6000-510000. Фабрично по подразбиране е 28000.
@ -254,4 +257,4 @@
<string name="no_calls">Разговори не могат да бъдат осъществени без разрешен достъп до микрофона.</string>
<string name="accounts_help">Порта и транспортния протокол на акаунта могат да бъдат посочени по избор, когато се създаде нов акаунт: потребителско име@домейн[:port] [;transport=udp|tcp|tls]. Ако е даден порт и транспортният протокол не е даден, транспортният протокол по подразбиране е udp. Ако порт не е даден и транспортният протокол е даден, портът по подразбиране е 5060 или 5061 (tls). Ако нито едното не е посочено и не е посочен изходящ прокси, регистраторът на акаунта (ако има такъв) се определя единствено въз основа на DNS информация от домейна.</string>
<string name="new_account">Нов Акаунт</string>
</resources>
</resources>

View File

@ -237,7 +237,6 @@
<string name="calls_call">volání</string>
<string name="tls_ca_file_help">Pokud je zaškrtnuto, byl nebo bude načten soubor obsahující certifikáty TLS takových certifikačních autorit, které nejsou součástí operačního systému Android. Ve verzi systému Android 9 se ze složky Download načte soubor s názvem \'ca_certs.crt\'.</string>
<string name="audio_settings">Nastavení zvuku</string>
<string name="aec_extended_filter_help">Pokud je zaškrtnuto, používá se rozšířený filtr pro potlačení ozvěny.</string>
<string name="opus_bit_rate_help">Průměrná maximální přenosová rychlost používaná zvukovým tokem Opus. Platné hodnoty jsou 6000-510000. Výchozí hodnota je 28000.</string>
<string name="opus_packet_loss">Očekávaná ztráta paketů Opus</string>
<string name="opus_packet_loss_help">Očekávaná procentuální ztráta paketů zvukového toku Opus od 0 do 100. Výchozí hodnota je 1. Hodnota 0 také vypíná funkci Opus Forward Error Correction (FEC).</string>
@ -246,9 +245,7 @@
<string name="default_call_volume">Výchozí hlasitost volání</string>
<string name="dark_theme_help">Vynutit tmavé téma</string>
<string name="failed_to_load_module">Nepodařilo se načíst modul.</string>
<string name="aec_help">Pokud je zaškrtnuto, dojde u hovoru k pokusu o potlačení ozvěny.</string>
<string name="audio_modules_help">Zvukové kodeky poskytované zaškrtnutými moduly jsou k dispozici pro použití v účtech.</string>
<string name="aec_extended_filter">Rozšířený filtr AEC</string>
<string name="invalid_opus_packet_loss">Neplatné procento ztráty paketů Opus</string>
<string name="invalid_opus_bitrate">Neplatný datový tok Opus</string>
<string name="default_call_volume_help">Pokud je nastaveno, je výchozí hlasitost zvuku hovoru na stupnici 1-10.</string>

View File

@ -359,7 +359,6 @@
<string name="video_codecs">Video Codecs</string>
<string name="audio_settings">Audioeinstellungen</string>
<string name="backup">Sicherungskopie</string>
<string name="aec_help">Falls angekreuzt, wird versucht, Echounterdrückung auf den Anruf anzuwenden.</string>
<string name="rtcp_mux">RTCP Multiplexverfahren</string>
<string name="invalid_opus_packet_loss">Ungültiger Opus Paketverlustprozentsatz</string>
<string name="default_call_volume">Standardmäßige Anruflautstärke</string>
@ -381,8 +380,6 @@
<string name="speaker_phone_help">Falls angekreuzt, wird die Lautsprecherfunktion automatisch aktiviert, wenn der Anruf beginnt.</string>
<string name="audio_modules_title">Audiomodule</string>
<string name="audio_modules_help">Audio Codecs, die von den ausgewählten Modulen bereitgestellt werden, sind für die Benutzer verfügbar.</string>
<string name="aec_extended_filter">AEC Erweiterter Filter</string>
<string name="aec_extended_filter_help">Falls angekreuzt, nutzt die Echounterdrückung den erweiterten Filter.</string>
<string name="opus_bit_rate">Opus Bitrate</string>
<string name="opus_bit_rate_help">Durchschnittliche maximale Bitrate, die vom Opus Audiostream genutzt wird. Gültige Werte sind 6000-510000. Die Werkseinstellung beträgt 28000.</string>
<string name="opus_packet_loss">Erwarteter Paketverlust von Opus</string>

View File

@ -139,7 +139,12 @@
<string name="start_automatically">Comenzar automáticamente</string>
<string name="start_automatically_help">Si está marcado, baresip se inicia automáticamente después de que el dispositivo se (re)inicia.</string>
<string name="listen_address">Dirección de escucha</string>
<string name="listen_address_help">Dirección IP y puerto de formulario \'address:port\' en el que escucha baresip para solicitudes SIP entrantes. Si la dirección IP es una dirección IPv6, debe escribirse dentro soportes []. La dirección IPv4 0.0.0.0 o la dirección IPv6 [::] hace que la escucha de baresip sea Direcciones disponibles. Si se deja vacío (predeterminado de fábrica), baresip escucha en el puerto 5060 de todas las direcciones disponibles.</string>
<string name="listen_address_help">Dirección IP y puerto de formulario \'address:port\' en el que escucha baresip
para solicitudes SIP entrantes. Si la dirección IP es una dirección IPv6, debe escribirse dentro
soportes []. La dirección IPv4 0.0.0.0 o la dirección IPv6 [::] hace que la escucha de baresip sea
Direcciones disponibles. Si se deja vacío (predeterminado de fábrica), baresip escucha en el puerto 5060 de
todas las direcciones disponibles.
</string>
<string name="invalid_listen_address">Dirección de escucha no válida</string>
<string name="dns_servers">Servidores DNS</string>
<string name="dns_servers_help">Lista separada por comas de direcciones de servidores DNS. Si no se da,
@ -155,7 +160,6 @@
<string name="tls_ca_file">Archivo de CA de TLS</string>
<string name="tls_ca_file_help">Si está marcada, se ha cargado o se cargará un archivo que contiene certificados TLS de dichas Autoridades de Certificación que no están incluidas en el SO Android. En las versión de Android 9, se carga un archivo llamado \'ca_certs.crt\' desde la carpeta Download.</string>
<string name="aec">Cancelación de eco acústico</string>
<string name="aec_help">Si está marcada, se intenta la cancelación del eco en el audio de la llamada.</string>
<string name="opus_bit_rate">Tasa de bits de Opus</string>
<string name="opus_bit_rate_help">Velocidad de bits máxima promedio utilizada por la transmisión de audio Opus.
Los valores válidos son 6000-510000. El valor predeterminado de fábrica es 28000.</string>
@ -284,8 +288,6 @@
<string name="video_size">Tamaño del fotograma de vídeo</string>
<string name="dark_theme_help">Forzar el tema de la pantalla oscura</string>
<string name="dark_theme">Tema oscuro</string>
<string name="aec_extended_filter_help">Si está marcada, la cancelación de eco está utilizando el filtro extendido.</string>
<string name="aec_extended_filter">Filtro extendido AEC</string>
<string name="verify_server_help">Si está marcada, baresip verifica los certificados TLS del Agente de Usuario SIP y de los Servidores Proxy SIP cuando se utiliza el transporte TLS.</string>
<string name="verify_server">Verificar los certificados del servidor</string>
<string name="transfer_request_to">Solicitud de transferencia de llamada a</string>
@ -451,4 +453,4 @@ También se pueden comunicar problemas.
<string name="microphone_gain_help">Multiplica el volumen del micrófono por este número decimal. El valor mínimo es 1.0 (predeterminado de fábrica) que desactiva la ganancia del micrófono. Los valores más altos pueden afectar negativamente a la calidad del audio.</string>
<string name="microphone_gain">Ganancia del micrófono</string>
<string name="invalid_microphone_gain">Valor de ganancia de micrófono no válido</string>
</resources>
</resources>

View File

@ -6,8 +6,10 @@
<string name="about_text">
<![CDATA[
<h1>Baresip-kirjastoon perustuva, äänipuhelut ja pikaviestit mahdollistava SIP-sovellus</h1>
<br>
<p>Juha Heinanen &lt;jh@tutpro.com&gt;</p>
<p>Versio %1$s</p>
<br>
<h2>Käyttövihjeitä</h2>
<ul>
<li>Tarkista, että baresip-sovelluksen Asetukset vastaavat tarpeitasi. Kunkin otsikon kosketus tarjoaa apua.</li>
@ -17,7 +19,6 @@
valkoinen (rekisteröintiä ei ole aktivoitu).</li> <li>Pisteen kosketus johtaa suoraan tilin asetuksiin.</li>
<li>Pyyhkäisy alas aikaansaa parhaillaan näkyvissä olevan tilin uudelleen rekisteröinnin.</li>
<li>Parhaillaan näkyvissä olevan tilin pitkä kosketus aktivoi tai passivoi tilin rekisteröinnin.</li>
<li>Pyyhkäisyllä vasemmalle/oikealle voit vaihtaa näkyvissä olevaa tiliä.</li>
<li>Voit valita edellisen puhelun uudelleen koskettamalla virheää luuria silloin, kun puhelun kohde on tyhjä.</li>
<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>
@ -25,10 +26,12 @@
<li>Katso lisätietoja <a href=https://github.com/juha-h/baresip-studio/wiki>Wiki</a>-sivulta.</li>
</ul>
<h2>Tietosuoja</h2>
Sovelluksen tietosuojakäytäntö on luettavissa <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">täältä</a>.
<p>Sovelluksen tietosuojakäytäntö on luettavissa <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">täältä</a>.</p>
<br>
<h2>Lähdekoodi</h2>
Lähdekoodi on saatavilla <a href=https://github.com/juha-h/baresip-studio>GitHub</a>:ssa,
missä voi myös raportoida virheistä.
<p>Lähdekoodi on saatavilla <a href=https://github.com/juha-h/baresip-studio>GitHub</a>:ssa,
missä voi myös raportoida virheistä.</p>
<br>
<h2>Lisenssit</h2>
<ul>
<li><b>BSD-3-Clause</b> except the following:</li>
@ -223,9 +226,9 @@
</string>
<!-- Accounts Activity -->
<string name="accounts">Tilit</string>
<string name="new_account">Uusi tili</string>
<string name="accounts_help">Kun uusi tili luodaan, voidaan haluttaessa antaa myös porttinumero
ja tiedonsiirtoprotokolla: &lt;käyttäjä&gt;@&lt;domain&gt;[:&lt;portti&gt;][;transport=udp|tcp|tls].
<string name="new_account">Uuden tilin SIP URI</string>
<string name="accounts_help">Uuden tilin SIP URI muotoa
&lt;käyttäjä&gt;@&lt;domain&gt;[:&lt;portti&gt;][;transport=udp|tcp|tls].
Jos &lt;portti&gt; on annettu, mutta protokollaa ei ole annettu, protokolla on udp.
Jos &lt;portti&gt; ei ole annettu, mutta protokolla on annettu, portti on joko 5060 tai 5061 (tls).
Jos kumpaakaan ei ole annettu eikä välityspalvelinta ole määritelty, tilin mahdollinen
@ -293,11 +296,11 @@
<!-- Codecs Activity -->
<string name="audio_codecs">Audion koodausmenetelmät</string>
<string name="audio_codecs_help">Luettelo audion koodausmenetelmistä prioriteettijärjestyksessä.
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pyyhkäisemällä oikealle.
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pitkällä painalluksella.
</string>
<string name="video_codecs">Videon koodausmenetelmät</string>
<string name="video_codecs_help">Luettelo videon koodausmenetelmistä prioriteettijärjestyksessä.
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pyyhkäisemällä oikealle.
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pitkällä painalluksella.
</string>
<string name="codec_action">Järjestä</string>
<!-- Config Activity -->
@ -365,9 +368,6 @@
</string>
<string name="failed_to_load_module">Modulin lataaminen epäonnistui.</string>
<string name="aec">Akustinen kaiun poisto (AKP)</string>
<string name="aec_help">Jos merkitty, kaikua yritetään poistaa ohjelmiston avulla.</string>
<string name="no_hw_aec">Laitteellasi ei voida poistaa kaikua laitteiston avulla.</string>
<string name="aec_extended_filter">AKP laajennettu suodatin</string>
<string name="microphone_gain">Mikrofonin äänikerroin</string>
<string name="microphone_gain_help">Kertoo mikrofonin äänen voimakkuuden tällä desimaaliluvulla.
Minimikerroin on 1.0 (tehdasasetus), mikä ei muuta mikrofonin äänen voimakkuutta. Suuremmat
@ -433,6 +433,8 @@
<string name="sip_or_tel_uri">SIP tai tel URI</string>
<string name="user_domain_or_number">käyttäjä@domain tai puhelinnumero</string>
<string name="favorite">Suosikki</string>
<string name="favorite_help">If ruksattu, kontakti näytetään muiden suosikkien
kanssa yhteystietolistan alussa</string>
<string name="invalid_contact">Virheellinen yhteystiedon nimi \'%1$s\'</string>
<string name="contact_already_exists">Yhteystieto \'%1$s\' on jo olemassa.</string>
<string name="invalid_contact_uri">Virheellinen SIP:n URI</string>
@ -470,6 +472,8 @@
<string name="invalid_sip_or_tel_uri">Virheellinen SIP tai puh. URI \'%1$s\'</string>
<string name="baresip" translatable="false">baresip</string>
<string name="android" translatable="false">Android</string>
<string name="hardware">Laitteisto</string>
<string name="software">Ohjelmisto</string>
<!-- Main Activity -->
<string name="backup">Tallenna</string>
<string name="restore">Palauta</string>

View File

@ -130,7 +130,12 @@
<string name="calls_add_delete_question">Do you want to add \'%1$s\' to contacts or delete %2$s from call history?
</string>
<string name="long_chat_question">Do you want to delete chat with peer \'%1$s\' or add peer to contacts?</string>
<string name="listen_address_help">IP address and port of form \'address:port\' at which baresip listens for incoming SIP requests. If IP address is an IPv6 address, it must be written inside brackets []. IPv4 address 0.0.0.0 or IPv6 address [::] makes baresip listen at all available addresses. If left empty (factory default), baresip listens at port 5060 of all available addresses.</string>
<string name="listen_address_help">IP address and port of form \'address:port\' at which baresip listens
for incoming SIP requests. If IP address is an IPv6 address, it must be written inside
brackets []. IPv4 address 0.0.0.0 or IPv6 address [::] makes baresip listen at all
available addresses. If left empty (factory default), baresip listens at port 5060 of
all available addresses.
</string>
<string name="dns_servers_help">Comma separated list of addresses of DNS servers. If not given,
DNS server addresses are obtained dynamically from the system. Each DNS address is of form
\'ip:port\' or \'ip\'. If port is omitted, it defaults to 53. If ip is an IPv6 address and
@ -290,8 +295,6 @@
<string name="tls_ca_file">TLS CA証明書ファイル</string>
<string name="audio_modules_title">オーディオモジュール</string>
<string name="failed_to_load_module">モジュールの読み込みに失敗しました</string>
<string name="aec_help">チェックを入れると、通話音声のエコーキャンセルを試みます</string>
<string name="aec_extended_filter">音響エコーキャンセル拡張フィルタ</string>
<string name="opus_bit_rate">Opusのビットレート</string>
<string name="opus_packet_loss">期待されるOpusのパケットロス</string>
<string name="invalid_opus_bitrate">Opusのビットレートが無効です</string>
@ -346,7 +349,6 @@
<string name="answer_mode_help">着信した電話にどのように応答するかを選択します。</string>
<string name="start_automatically_help">チェックを入れると、デバイスの再起動後に自動的にbaresipが起動します</string>
<string name="aec">音響エコーキャンセル</string>
<string name="aec_extended_filter_help">チェックを入れると、エコーキャンセルは拡張フィルタを使用しています。</string>
<string name="default_call_volume_help">設定されている場合、デフォルトの通話音声の音量は110段階です</string>
<string name="dark_theme_help">ダークテーマを強制する</string>
<string name="debug_help">チェックを入れると、デバッグおよび情報レベルのログメッセージをLogcatに提供します。</string>
@ -363,4 +365,4 @@
<string name="transfer_request_query"> %1$s への通話転送を許可しますか?</string>
<string name="backed_up">アプリケーションデータがダウンロードフォルダ %1$s にバックアップされました</string>
<string name="no_calls">マイクへの権限付与がなく電話をかけたり、応答したりすることはできません。</string>
</resources>
</resources>

View File

@ -130,7 +130,6 @@
<string name="dns_servers_help">Kommainndelt liste over adresser til DNS-tjenere. Hvis ikke angitt, vil DNS-tjeneradresser hentes dynamisk fra systemet. Hver DNS-adresse er utførelsen \"ip:port\" eller \"ip\". Hvis porten utelates, brukes forvalget 53. Hvis IP-en er en IPv6-adresse, og port også angis, må IP-en skrives i hakeparenteser []. Som etksempel, list \'8.8.8.8:53,[2001:4860:4860::8888]:53\' peker til en IPv4-adresser og IPv6-adresser for offentlige Google DNS-tjenere.</string>
<string name="failed_to_load_module">Klarte ikke å laste inn modul.</string>
<string name="aec">Kansellering av akustisk ekko</string>
<string name="aec_help">Prøver å unngå ekko under samtaler.</string>
<string name="opus_bit_rate_help">Gjennomsnittlig maksimal bitrate ved bruk av Opus-lydstrøm. Gyldig verdi er 6000-510000. Fabrikksforvalg er 28000.</string>
<string name="opus_packet_loss">Forventet Opus-pakketap</string>
<string name="opus_packet_loss_help">Forventet prosentvis pakketap for Opus-lydstrøm. Gyldige verdier fra 0100. Fabrikksforvalg er 0, som skrur av Opus-fremtidsfeilkorrigering (FEC).</string>
@ -231,4 +230,4 @@
<string name="time">Tid</string>
<string name="calls_duration">Varighet</string>
<string name="audio_settings">Lydinnstillinger</string>
</resources>
</resources>

View File

@ -91,7 +91,6 @@
<string name="opus_packet_loss">Perda de pacotes Opus esperada</string>
<string name="opus_bit_rate_help">Taxa de bits máxima média usada pelo fluxo de áudio do Opus. Os valores válidos são 6000-510000. O padrão de fábrica é 28000.</string>
<string name="opus_bit_rate">Taxa de Bits Opus</string>
<string name="aec_help">Se estiver marcado, o cancelamento de eco baseado em software está em uso.</string>
<string name="aec">Cancelamento de Eco Acústico</string>
<string name="failed_to_load_module">Falha ao carregar módulo.</string>
<string name="audio_modules_help">Codecs de áudio providos pelos módulos selecionados estão disponíveis para uso das contas.</string>
@ -249,8 +248,6 @@
<string name="confirmation">Confirmação</string>
<string name="video_size_help">Tamanho dos quadros de vídeo que foram transmitidos (largura x altura)</string>
<string name="video_size">Tamanho do Quadro do Vídeo</string>
<string name="aec_extended_filter_help">Caso esteja marcado, o cancelamento do eco estará utilizando um filtro estendido.</string>
<string name="aec_extended_filter">Filtro Estendido do AEC</string>
<string name="invalid_stun_password">Senha inválida \'%1$s\'</string>
<string name="stun_password_help">Senha, caso seja necessário pelo servidor STUN/TURN</string>
<string name="stun_password">Senha STUN/TURN</string>
@ -425,5 +422,4 @@
<string name="invalid_microphone_gain">Valor inválido do ganho do microfone</string>
<string name="microphone_gain">Ganho do microfone</string>
<string name="microphone_gain_help">Multiplique o volume do microfone por esse número decimal. O valor mínimo é 1,0 (padrão de fábrica), que desativa o ganho do microfone. Valores maiores podem afetar negativamente a qualidade do áudio.</string>
<string name="no_hw_aec">O cancelador de eco baseado em hardware não pode ser usado em seu dispositivo.</string>
</resources>

View File

@ -43,7 +43,6 @@
<string name="chat">Mensagem do Chat</string>
<string name="contact_action_question">Quer ligar ou enviar mensagem para \'%1$s\'\?</string>
<string name="tls_ca_file">Ficheiro TLS CA</string>
<string name="aec_extended_filter_help">Caso esteja marcado, o cancelamento do eco estará utilizando um filtro estendido.</string>
<string name="sip_uri_of_another_proxy_server">URI SIP de outro Servidor Proxy</string>
<string name="contact_already_exists">Contato \'%1$s\' já existe.</string>
<string name="sip_uri_of_proxy_server">URI SIP do Servidor Proxy</string>
@ -122,14 +121,12 @@
<string name="no_messages">Não tem mensagens</string>
<string name="duration">Duração: %1$d (segs)</string>
<string name="transferring_call_to_dots">Transferindo chamada para …</string>
<string name="aec_help">Se marcado, cancelamento de eco é tentado no áudio da chamada.</string>
<string name="call_info">Informações da Chamada</string>
<string name="chats">Histórico de Mensagens</string>
<string name="call_is_secure">A chamada é SEGURA e o par é VERIFICADO! Deseja desverificar o par\?</string>
<string name="opus_bit_rate_help">Taxa de bits máxima média usada pelo fluxo de áudio do Opus. Os valores válidos são 6000-510000. A predefinição de fábrica é 28000.</string>
<string name="restart_request">Solicitação para Reinicialização</string>
<string name="register">Registo</string>
<string name="aec_extended_filter">Filtro Estendido do AEC</string>
<string name="restore">Restaurar</string>
<string name="opus_packet_loss">Perda de pacotes Opus esperada</string>
<string name="transfer_request_query">Aceita transferir esta chamada para \'%1$s\'\?</string>
@ -325,4 +322,4 @@
<string name="tone_country_help">Toque da chamada do país, espera e tom de chamada ocupada</string>
<string name="rel_100">Respostas provisórias confiáveis</string>
<string name="rel_100_help">Se for marcado, indica suporte para respostas provisórias confiáveis (RFC 3262).</string>
</resources>
</resources>

View File

@ -62,7 +62,6 @@
<string name="listen_address">Adresă de conectare</string>
<string name="dns_servers">Servere DNS</string>
<string name="aec">Anularea acustică a ecoului</string>
<string name="aec_help">Dacă este bifat, se încearcă eliminarea ecoului în timpul apelurilor.</string>
<string name="opus_bit_rate">Rată de eșantionare Opus</string>
<string name="default_call_volume">Volum implicit de apel</string>
<string name="default_call_volume_help">Dacă este setat, volumul audio de apel implicit între 110.</string>

View File

@ -134,7 +134,6 @@
<string name="account_exists">Профиль \'%1$s\' уже существует.</string>
<string name="accounts">Профили</string>
<string name="aec">Подавление акустического эха</string>
<string name="aec_help">Если выбрано - работает подавление эха.</string>
<string name="and">и</string>
<string name="answer_mode">Режим ответа</string>
<string name="answer_mode_help">Выберите как отвечать на входящие вызовы.</string>
@ -181,8 +180,6 @@
<string name="opus_packet_loss">Ожидаемая потеря пакетов Opus</string>
<string name="opus_bit_rate_help">Средняя максимальная скорость передачи данных, используемая в аудиопотоке Opus. Допустимые значения: 6000-510000. Значение по умолчанию - 28000.</string>
<string name="opus_bit_rate">Скорость передачи данных Opus</string>
<string name="aec_extended_filter_help">Если этот флажок установлен, для подавления эха используется расширенный фильтр.</string>
<string name="aec_extended_filter">Расширенный фильтр AEC</string>
<string name="audio_modules_help">Аудиокодеки, предоставленные выбранными модулями, доступны для использования в аккаунтах.</string>
<string name="tls_ca_file_help">При отметке файл с TLS-сертификатами необходимых центров сертификации был или будет загружен. В Android версии 9 и ниже файл называется \'ca_certs.crt\' и берётся из каталога загрузок.</string>
<string name="tls_ca_file">Файл TLS CA</string>
@ -328,4 +325,4 @@
<string name="rel_100">Надёжные предварительные ответы</string>
<string name="appear_on_top_permission">Для автоматического запуска требуется разрешение отображения поверх других приложений.</string>
<string name="video_fps">Кадровая частота</string>
</resources>
</resources>

View File

@ -219,12 +219,9 @@
<string name="transfer_request">Kopplingsbegäran</string>
<string name="default_call_volume_help">Kryssa i för att använda en förvald ljudvolym, 110, vid samtal.</string>
<string name="accept">Acceptera</string>
<string name="aec_extended_filter">Utökat filter vid ekoutsläckning</string>
<string name="android_contact_help">Kryssa i för att lägga till kontakten till dina Android-kontakter.</string>
<string name="sip_trace_help">Kryssa i för att inkludera SIP-meddelanden i Logcat-meddelanden när Felsökning är aktiverat. Funktionen inaktiveras automatiskt när baresip startar.</string>
<string name="debug_help">Kryssa i för att skicka felsöknings- och info-meddelanden till Logcat.</string>
<string name="aec_extended_filter_help">Kryssa i för att använda ett utökat filter vid ekoutsläckning.</string>
<string name="aec_help">Kryssa i för att aktivera ekoutsläckning för samtalsljud.</string>
<string name="tls_ca_file_help">Kryssa i för att ladda TLS-certifikat för certifikatauktoriteter från fil. I Android 9 och tidigare versioner laddas en fil med namnet \'ca_certs.crt\' från katalogen Nedladdningar.</string>
<string name="verify_server_help">Kryssa i om baresip ska verifiera TLS-certifikat för SIP user agent och SIP proxyserver när transportprotokollet TLS används. TLS CA-fil måste då också laddas.</string>
<string name="tls_certificate_file_help">Kryssa i för att ladda TLS-certifikat och privat nyckel för den här baresip-instansen från fil. I Android 9 och tidigare versioner laddas en fil med namnet \'cert.pem\' från katalogen Nedladdningar. Av säkerhetsskäl tas filen bort efter att den lästs in.</string>
@ -312,4 +309,4 @@
<string name="reg_int_help">Avgör hur ofta (i sekunder) baresip skickar REGISTER-förfrågningar. Giltiga värden är från 60 till 3600.</string>
<string name="audio_delay_help">Tid (i millisekunder) att vänta på ljud från motparten när ett samtal upprättats. Sätt till ett högre värde om du saknar ljud från motparten i början av samtalet.</string>
<string name="audio_and_video_permissions">baresip+ behöver behörigheten \"Mikrofon\" för röstsamtal, behörigheten \"Kamera\" för videosamtal, behörigheten \"Enheter i närheten\" för upptäckt av mikrofon/högtalare via blåtand och behörigheten \"Aviseringar\" för att skicka aviseringar.</string>
</resources>
</resources>

View File

@ -171,8 +171,6 @@
<string name="audio_settings">ஆடியோ அமைப்புகள்</string>
<string name="tls_ca_file_help">சரிபார்க்கப்பட்டால், ஆண்ட்ராய்டு OS இல் சேர்க்கப்படாத அத்தகைய சான்றிதழ் அதிகாரிகளின் TLS சான்றிதழ்களைக் கொண்ட ஒரு கோப்பு அல்லது ஏற்றப்படும். ஆண்ட்ராய்டு பதிப்பு 9 இல், \'Ca_certs.crt\' என்ற கோப்பு பதிவிறக்க கோப்புறையிலிருந்து ஏற்றப்படுகிறது.</string>
<string name="speaker_phone">அவைத்தலைவர் தொலைபேசி</string>
<string name="aec_extended_filter">AEC நீட்டிக்கப்பட்ட வடிகட்டி</string>
<string name="aec_extended_filter_help">சரிபார்க்கப்பட்டால், எதிரொலி ரத்துசெய்தல் நீட்டிக்கப்பட்ட வடிப்பானைப் பயன்படுத்துகிறது.</string>
<string name="microphone_gain_help">இந்த தசம எண்ணால் மைக்ரோஃபோன் அளவைப் பெருக்கவும். மைக்ரோஃபோன் ஆதாயத்தை முடக்கும் குறைந்தபட்ச மதிப்பு 1.0 (தொழிற்சாலை இயல்புநிலை) ஆகும். பெரிய மதிப்புகள் ஆடியோ தரத்தை எதிர்மறையாக பாதிக்கலாம்.</string>
<string name="speaker_phone_help">சரிபார்க்கப்பட்டால், அழைப்பு தொடங்கும் போது ச்பீக்கர் தொலைபேசி தானாக இயக்கப்படும்.</string>
<string name="audio_modules_title">ஆடியோ தொகுதிகள்</string>

View File

@ -8,8 +8,10 @@
<string name="about_text">
<![CDATA[
<h1>Baresip library based SIP User Agent</h1>
<br>
<p>Juha Heinanen &lt;jh@tutpro.com&gt;</p>
<p>Version %1$s</p>
<br>
<h2>Usage Hints</h2>
<ul>
<li>Check that default values in baresip\'s Settings meet your needs
@ -21,19 +23,19 @@
<li>Touch on the dot leads directly to account configuration.</li>
<li>Swipe down gesture causes re-registration of the currently shown account.</li>
<li>Long touch on currently shown account enables or disables account\'s registration.</li>
<li>Swipe left/right gesture toggles between the accounts.</li>
<li>Previous call party can be reselect by touching the call icon when Callee is empty.</li>
<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>See <a href="https://github.com/juha-h/baresip-studio/wiki">Wiki</a> for more
information.</li>
<li>See <a href="https://github.com/juha-h/baresip-studio/wiki">Wiki</a> for more information.</li>
</ul>
<h2>Privacy Policy</h2>
Privacy policy is available <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">here</a>.
<p>Privacy policy is available <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">here</a>.</p>
<br>
<h2>Source code</h2>
Source code is available at <a href="https://github.com/juha-h/baresip-studio">GitHub</a>,
where also issues can be reported.
<p>Source code is available at <a href="https://github.com/juha-h/baresip-studio">GitHub</a>,
where also issues can be reported.</p>
<br>
<h2>Licenses</h2>
<ul>
<li><b>BSD-3-Clause</b> except the following:</li>
@ -210,10 +212,10 @@
</string>
<!-- Accounts Activity -->
<string name="accounts">Accounts</string>
<string name="new_account">New Account</string>
<string name="accounts_help">When a new account is created, account\'s port number and transport
protocol may be optionally given: &lt;user&gt;@&lt;domain&gt;[:&lt;port&gt;][;transport=udp|tcp|tls].
If &lt;port&gt; is given and transport protocol is not given, transport protocol defaults to udp.
<string name="new_account">SIP URI of New Account</string>
<string name="accounts_help">SIP URI of new account of form
&lt;user&gt;@&lt;domain&gt;[:&lt;port&gt;][;transport=udp|tcp|tls].
If &lt;port&gt; is given and transport protocol is not given, transport protocol defaults to UDP.
If &lt;port&gt; is not given and transport protocol is given, &lt;port&gt; defaults to 5060 or 5061 (TLS).
If neither is given and no outbound proxy is specified, account\'s registrar (if any) is
determined solely based on domain\'s DNS information.
@ -278,10 +280,10 @@
<!-- Codecs Activity -->
<string name="audio_codecs">Audio Codecs</string>
<string name="audio_codecs_help">List of audio codecs in priority order. Drag to reorder,
swipe right to enable or disable.</string>
long click to enable or disable.</string>
<string name="video_codecs">Video Codecs</string>
<string name="video_codecs_help">List of video codecs in priority order. Drag to reorder,
swipe right to enable or disable.</string>
long click to enable or disable.</string>
<string name="codec_action">Reorder</string>
<!-- Config Activity -->
<string name="configuration">Settings</string>
@ -340,10 +342,6 @@
available for use by the accounts.</string>
<string name="failed_to_load_module">Failed to load module.</string>
<string name="aec">Acoustic Echo Cancellation</string>
<string name="aec_help">If checked, software based echo cancellation is in use.</string>
<string name="no_hw_aec">Hardware based echo canceller cannot be used on you device.</string>
<string name="aec_extended_filter">AEC Extended Filter</string>
<string name="aec_extended_filter_help">If checked, echo cancellation is using extended filter.</string>
<string name="microphone_gain">Microphone Gain</string>
<string name="microphone_gain_help">Multiply microphone volume by this decimal number. Minimum
value is 1.0 (factory default) that disables microphone gain. Larger values may negatively
@ -413,6 +411,8 @@
<string name="sip_or_tel_uri">SIP or tel URI</string>
<string name="user_domain_or_number">user@domain or telephone number</string>
<string name="favorite">Favorite</string>
<string name="favorite_help">If checked, contact is shown among other favorites
on top of contacts list.</string>
<string name="invalid_contact">Invalid contact name \'%1$s\'</string>
<string name="contact_already_exists">Contact \'%1$s\' already exists.</string>
<string name="invalid_contact_uri">Invalid SIP URI</string>
@ -452,6 +452,8 @@
<string name="invalid_sip_or_tel_uri">Invalid SIP or tel URI \'%1$s\'</string>
<string name="baresip" translatable="false">baresip</string>
<string name="android" translatable="false">Android</string>
<string name="hardware">Hardware</string>
<string name="software">Software</string>
<!-- Main Activity -->
<string name="backup">Backup</string>
<string name="restore">Restore</string>

View File

@ -1,50 +1,23 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="actionBarTheme">@style/ActionBar</item>
<item name="actionOverflowMenuStyle">@style/Spinner</item>
<item name="android:dropDownListViewStyle">@style/Spinner</item>
<item name="android:colorBackground">@color/colorBackground</item>
<item name="android:windowBackground">@color/colorBackground</item>
<item name="android:statusBarColor">@color/colorBackground</item>
</style>
<style name="AppTheme.Main" parent="AppTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="ActionBar" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
<item name="android:colorBackground">@color/colorSecondaryDark</item>
<item name="colorSurface">@color/colorPrimary</item>
<item name="android:textColor">@color/colorLight</item>
<item name="android:textColorPrimary">@color/colorLight</item>
<item name="android:textColorSecondary">@color/colorLight</item>
</style>
<style name="Spinner" parent="Widget.AppCompat.ListView.DropDown">
<item name="android:popupBackground">@color/colorPopupBackground</item>
<item name="android:textColor">@color/colorLight</item>
<item name="android:dividerHeight">1dp</item>
<item name="android:divider">@color/colorSpinnerDivider</item>
</style>
<style name="AlertDialogTheme">
<style name="AlertDialogTheme" parent="@style/ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="materialAlertDialogTitleTextStyle">@style/Alert.Title</item>
<item name="materialAlertDialogBodyTextStyle">@style/Alert.Message</item>
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
<item name="buttonBarNegativeButtonStyle">@style/Alert.Button.Positive</item>
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
<item name="materialAlertDialogBodyTextStyle">@style/Alert.Message</item>
<item name="materialAlertDialogTitleTextStyle">@style/Alert.Title</item>
<item name="shapeAppearanceOverlay">@style/DialogCorners</item>
<item name="android:background">@color/colorGrayLight</item>
<item name="android:textSize">14sp</item>
</style>
<style name="DialogCorners">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
<style name="Alert.Title" parent="@style/MaterialAlertDialog.Material3.Title.Text">
<item name="android:textColor">@color/colorAlert</item>
<item name="android:textSize">20sp</item>
</style>
<style name="Alert.Message" parent="@style/MaterialAlertDialog.Material3.Body.Text">
<item name="android:textColor">@color/colorDark</item>
<item name="android:textSize">16sp</item>
</style>
<style name="Alert.Button.Positive" parent="Widget.Material3.Button.TextButton">
@ -61,14 +34,4 @@
<item name="android:textSize">14sp</item>
</style>
<style name="Alert.Title" parent="@style/MaterialAlertDialog.Material3.Title.Text">
<item name="android:textColor">@color/colorAlert</item>
<item name="android:textSize">20sp</item>
</style>
<style name="Alert.Message" parent="@style/MaterialAlertDialog.Material3.Body.Text">
<item name="android:textColor">@color/colorStrong</item>
<item name="android:textSize">16sp</item>
</style>
</resources>

View File

@ -8,7 +8,7 @@ buildscript {
}
dependencies {
// noinspection AndroidGradlePluginVersion
classpath 'com.android.tools.build:gradle:8.8.1'
classpath libs.gradle.v881
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

68
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,68 @@
[versions]
activityCompose = "1.10.1"
appcompat = "1.7.0"
cardview = "1.0.0"
coilCompose = "2.5.0"
composeBom = "2025.03.01"
constraintlayout = "2.2.1"
coreKtx = "1.15.0"
exifinterface = "1.4.0"
foundation = "1.7.8"
fragmentKtx = "1.8.6"
gradle = "8.9.1"
gradleVersion = "8.9.1"
kotlin = "2.0.21"
kotlinStdlibJdk8 = "2.1.0"
kotlinxCoroutinesAndroid = "1.9.0"
lifecycleExtensions = "2.2.0"
lifecycleViewmodelCompose = "2.8.7"
localbroadcastmanager = "1.1.0"
material = "1.12.0"
media = "1.7.0"
preferenceKtx = "1.2.1"
swiperefreshlayout = "1.1.0"
ui = "1.8.0-rc02"
foundationAndroid = "1.7.8"
toolsCore = "1.0.0-alpha13"
runtimeLivedata = "1.7.8"
uiTestAndroid = "1.7.8"
composeMaterial = "1.7.8"
composeMaterial3 = "1.3.1"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityCompose" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" }
androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleExtensions" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-localbroadcastmanager = { module = "androidx.localbroadcastmanager:localbroadcastmanager", version.ref = "localbroadcastmanager" }
androidx-media = { module = "androidx.media:media", version.ref = "media" }
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-ui = { module = "androidx.compose.ui:ui", version.ref = "ui" }
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
gradle-v881 = { module = "com.android.tools.build:gradle", version.ref = "gradleVersion" }
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlinStdlibJdk8" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
material = { module = "com.google.android.material:material", version.ref = "material" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-foundation-android = { group = "androidx.compose.foundation", name = "foundation-android", version.ref = "foundationAndroid" }
androidx-tools-core = { group = "androidx.privacysandbox.tools", name = "tools-core", version.ref = "toolsCore" }
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
androidx-ui-test-android = { group = "androidx.compose.ui", name = "ui-test-android", version.ref = "uiTestAndroid" }
androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "composeMaterial" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "composeMaterial3" }
[plugins]
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View File

@ -1,6 +1,6 @@
#Mon Jan 30 05:45:34 EET 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists