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
-79
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
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)
}
+15 -6
View File
@@ -45,7 +45,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:largeHeap="true" android:largeHeap="true"
android:theme="@style/AppTheme" android:theme="@style/Theme.Material3.DayNight.NoActionBar"
tools:remove="android:appComponentFactory" > tools:remove="android:appComponentFactory" >
<activity <activity
@@ -53,7 +53,6 @@
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/AppTheme.Main"
android:windowSoftInputMode="adjustResize" > android:windowSoftInputMode="adjustResize" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -78,7 +77,7 @@
<activity <activity
android:name=".AccountsActivity" android:name=".AccountsActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustResize"
android:label="@string/accounts" android:label="@string/accounts"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" >
</activity> </activity>
@@ -86,7 +85,7 @@
<activity <activity
android:name=".AccountActivity" android:name=".AccountActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustResize"
android:label="@string/account" > android:label="@string/account" >
</activity> </activity>
@@ -94,6 +93,7 @@
android:name=".CodecsActivity" android:name=".CodecsActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/codecs" android:label="@string/codecs"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".AccountActivity" > android:parentActivityName=".AccountActivity" >
</activity> </activity>
@@ -101,12 +101,14 @@
android:name=".ContactsActivity" android:name=".ContactsActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/contacts" android:label="@string/contacts"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" >
</activity> </activity>
<activity <activity
android:name=".ContactActivity" android:name=".BaresipContactActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustResize"
android:label="@string/contact" android:label="@string/contact"
android:parentActivityName=".ContactsActivity" > android:parentActivityName=".ContactsActivity" >
</activity> </activity>
@@ -115,6 +117,7 @@
android:name=".AndroidContactActivity" android:name=".AndroidContactActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/contact" android:label="@string/contact"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".ContactsActivity" > android:parentActivityName=".ContactsActivity" >
</activity> </activity>
@@ -122,6 +125,7 @@
android:name=".ConfigActivity" android:name=".ConfigActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/configuration" android:label="@string/configuration"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" >
</activity> </activity>
@@ -129,6 +133,7 @@
android:name=".AudioActivity" android:name=".AudioActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/audio_settings" android:label="@string/audio_settings"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".ConfigActivity" > android:parentActivityName=".ConfigActivity" >
</activity> </activity>
@@ -136,6 +141,7 @@
android:name=".CallsActivity" android:name=".CallsActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/call_history" android:label="@string/call_history"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" >
</activity> </activity>
@@ -143,19 +149,21 @@
android:name=".CallDetailsActivity" android:name=".CallDetailsActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/call_details" android:label="@string/call_details"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".CallsActivity" > android:parentActivityName=".CallsActivity" >
</activity> </activity>
<activity <activity
android:name=".ChatsActivity" android:name=".ChatsActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustResize"
android:label="@string/chats" android:label="@string/chats"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" >
</activity> </activity>
<activity <activity
android:name=".ChatActivity" android:name=".ChatActivity"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/chat" > android:label="@string/chat" >
</activity> </activity>
@@ -164,6 +172,7 @@
android:name=".AboutActivity" android:name=".AboutActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/about_title" android:label="@string/about_title"
android:windowSoftInputMode="adjustResize"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" >
</activity> </activity>
+18 -10
View File
@@ -1799,10 +1799,11 @@ JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_module_1unload(
(*env)->ReleaseStringUTFChars(env, javaModule, native_module); (*env)->ReleaseStringUTFChars(env, javaModule, native_module);
} }
AAudioStream *AAudio_stream = NULL;
JNIEXPORT jint JNICALL 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; AAudioStreamBuilder *builder = NULL;
AAudioStream *stream = NULL;
jint sessionId = -1; jint sessionId = -1;
if (AAudio_createStreamBuilder(&builder) != AAUDIO_OK) { 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_setChannelCount(builder, 1);
AAudioStreamBuilder_setSessionId(builder, AAUDIO_SESSION_ID_ALLOCATE); AAudioStreamBuilder_setSessionId(builder, AAUDIO_SESSION_ID_ALLOCATE);
if (AAudioStreamBuilder_openStream(builder, &stream) == AAUDIO_OK) { if (AAudioStreamBuilder_openStream(builder, &AAudio_stream) == AAUDIO_OK)
sessionId = AAudioStream_getSessionId(stream); sessionId = AAudioStream_getSessionId(AAudio_stream);
struct timespec td = { else {
.tv_nsec = 100*1000*1000 /* 100ms */ LOGE("Failed to open AAudio stream\n");
}; AAudio_stream = NULL;
nanosleep(&td, NULL);
AAudioStream_close(stream);
} }
AAudioStreamBuilder_delete(builder); AAudioStreamBuilder_delete(builder);
return sessionId; 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;
}
}
@@ -1,25 +1,43 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.core.text.HtmlCompat
import androidx.appcompat.app.AppCompatActivity
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.core.graphics.Insets import androidx.activity.compose.setContent
import androidx.core.view.ViewCompat import androidx.activity.enableEdgeToEdge
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.background
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.layout.fillMaxSize
import androidx.core.view.updatePadding import androidx.compose.foundation.layout.padding
import com.tutpro.baresip.databinding.ActivityAboutBinding 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() { class AboutActivity : ComponentActivity() {
private lateinit var binding: ActivityAboutBinding
private val onBackPressedCallback = object : OnBackPressedCallback(true) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -30,31 +48,78 @@ class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat -> enableEdgeToEdge()
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
Utils.addActivity("about") Utils.addActivity("about")
val text = String.format(getString(R.string.about_text), val aboutTitle = String.format(getString(R.string.about_title))
val aboutText = String.format(getString(R.string.about_text),
BuildConfig.VERSION_NAME) BuildConfig.VERSION_NAME)
binding.aboutText.text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)
binding.aboutText.movementMethod = LinkMovementMethod.getInstance() setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AboutContent(aboutTitle, aboutText) { goBack() }
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
@@ -67,9 +132,8 @@ class AboutActivity : AppCompatActivity() {
private fun goBack() { private fun goBack() {
BaresipService.activities.remove("about") BaresipService.activities.remove("about")
setResult(Activity.RESULT_CANCELED, Intent()) setResult(RESULT_CANCELED, Intent())
finish() finish()
} }
} }
@@ -1,6 +1,8 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.content.Context import android.content.Context
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import java.net.URLEncoder import java.net.URLEncoder
@@ -8,10 +10,10 @@ import java.net.URLDecoder
class Account(val accp: Long) { class Account(val accp: Long) {
var nickName = "" val nickName: MutableState<String> = mutableStateOf("")
var displayName = Api.account_display_name(accp) var displayName = Api.account_display_name(accp)
val aor = Api.account_aor(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 authUser = Api.account_auth_user(accp)
var authPass = Api.account_auth_pass(accp) var authPass = Api.account_auth_pass(accp)
var outbound = ArrayList<String>() var outbound = ArrayList<String>()
@@ -68,7 +70,7 @@ class Account(val accp: Long) {
val extra = Api.account_extra(accp) val extra = Api.account_extra(accp)
if (Utils.paramExists(extra, "nickname")) if (Utils.paramExists(extra, "nickname"))
nickName = Utils.paramValue(extra,"nickname") nickName.value = Utils.paramValue(extra,"nickname")
if (Utils.paramExists(extra, "regint")) if (Utils.paramExists(extra, "regint"))
configuredRegInt = Utils.paramValue(extra,"regint").toInt() configuredRegInt = Utils.paramValue(extra,"regint").toInt()
callHistory = Utils.paramValue(extra,"call_history") == "" callHistory = Utils.paramValue(extra,"call_history") == ""
@@ -151,8 +153,8 @@ class Account(val accp: Long) {
var extra = "" var extra = ""
if (nickName != "") if (nickName.value != "")
extra += ";nickname=$nickName" extra += ";nickname=${nickName.value}"
if (!callHistory) if (!callHistory)
extra += ";call_history=no" extra += ";call_history=no"
@@ -207,6 +209,13 @@ class Account(val accp: Long) {
return aor.split("@")[1] return aor.split("@")[1]
} }
fun text(): String {
return if (nickName.value != "")
nickName.value
else
aor.split(":")[1].substringBefore(";")
}
private fun removeAudioCodecsStartingWith(prefix: String) { private fun removeAudioCodecsStartingWith(prefix: String) {
val newCodecs = ArrayList<String>() val newCodecs = ArrayList<String>()
for (acSpec in audioCodec) for (acSpec in audioCodec)
@@ -229,14 +238,14 @@ class Account(val accp: Long) {
fun accounts(): ArrayList<Account> { fun accounts(): ArrayList<Account> {
val res = ArrayList<Account>() val res = ArrayList<Account>()
for (ua in BaresipService.uas) { for (ua in BaresipService.uas.value) {
res.add(ua.account) res.add(ua.account)
} }
return res return res
} }
fun ofAor(aor: String): Account? { fun ofAor(aor: String): Account? {
for (ua in BaresipService.uas) for (ua in BaresipService.uas.value)
if (ua.account.aor == aor) return ua.account if (ua.account.aor == aor) return ua.account
return null return null
} }
@@ -263,8 +272,8 @@ class Account(val accp: Long) {
} }
fun uniqueNickName(nickName: String): Boolean { fun uniqueNickName(nickName: String): Boolean {
for (ua in BaresipService.uas) for (ua in BaresipService.uas.value)
if (ua.account.nickName == nickName) if (ua.account.nickName.value == nickName)
return false return false
return true return true
} }
File diff suppressed because it is too large Load Diff
@@ -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
}
}
@@ -1,3 +0,0 @@
package com.tutpro.baresip
class AccountRow(val aor: String, val action: Int)
@@ -1,27 +1,84 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.os.SystemClock
import android.view.MenuItem import androidx.activity.ComponentActivity
import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.activity.enableEdgeToEdge
import androidx.core.graphics.Insets import androidx.compose.foundation.Image
import androidx.core.view.ViewCompat import androidx.compose.foundation.background
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.clickable
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.layout.Arrangement
import androidx.core.view.updatePadding import androidx.compose.foundation.layout.Column
import com.tutpro.baresip.databinding.ActivityAccountsBinding 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 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) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -32,106 +89,380 @@ class AccountsActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAccountsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat -> enableEdgeToEdge()
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
}
if (!Utils.isDarkTheme(this)) val title = String.format(getString(R.string.accounts))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
aor = intent.getStringExtra("aor")!! aor = intent.getStringExtra("aor")!!
Utils.addActivity("accounts,$aor") Utils.addActivity("accounts,$aor")
val listView = binding.accounts mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF",
generateAccounts() "srtp-mand" to "SRTP-MAND", "srtp" to "SRTP", "" to "--")
alAdapter = AccountListAdapter(this, accounts)
listView.adapter = alAdapter
val accountRequest = mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
val addAccountButton = binding.addAccount setContent {
val newAorView = binding.newAor AppTheme {
addAccountButton.setOnClickListener { Surface(
val aor = newAorView.text.toString().trim() modifier = Modifier.fillMaxSize(),
if (!Utils.checkAor("sip:$aor")) { color = LocalCustomColors.current.background
Log.d(TAG, "Invalid Address of Record $aor") ) {
Utils.alertView(this, getString(R.string.notice), AccountsScreen(this, title) { goBack() }
String.format(getString(R.string.invalid_aor), aor))
return@setOnClickListener
} }
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) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { @OptIn(ExperimentalMaterial3Api::class)
@Composable
if (BaresipService.activities.indexOf("accounts,$aor") == -1) fun AccountsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
return true Scaffold(
modifier = Modifier.fillMaxHeight()
when (item.itemId) { .imePadding()
.safeDrawingPadding(),
R.id.help -> { containerColor = LocalCustomColors.current.background,
Utils.alertView(this@AccountsActivity, getString(R.string.new_account), topBar = {
getString(R.string.accounts_help)) TopAppBar(
return true 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)
},
)
} }
android.R.id.home -> { @Composable
goBack() fun AccountsContent(ctx: Context, contentPadding: PaddingValues) {
return true 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)
)
}
}
}
}
} }
} }
return super.onOptionsItemSelected(item) @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()
}
}
),
)
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { private fun createNew(newAor: String): Account? {
menuInflater.inflate(R.menu.accounts_menu, menu)
return true 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() { private fun goBack() {
BaresipService.activities.remove("accounts,$aor") BaresipService.activities.remove("accounts,$aor")
setResult(Activity.RESULT_CANCELED, Intent()) setResult(RESULT_CANCELED, Intent())
finish() finish()
} }
@@ -142,19 +473,11 @@ class AccountsActivity : AppCompatActivity() {
companion object { 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() { fun saveAccounts() {
var accounts = "" var accounts = ""
for (a in Account.accounts()) accounts = accounts + a.print() + "\n" 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'") // Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
} }
@@ -1,31 +1,64 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import androidx.activity.ComponentActivity
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.setContent
import androidx.cardview.widget.CardView import androidx.activity.enableEdgeToEdge
import com.tutpro.baresip.databinding.ActivityAndroidContactBinding 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 aor: String
private lateinit var name: String
private var index = 0
private var color = 0 private var color = 0
private val onBackPressedCallback = object : OnBackPressedCallback(true) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
@@ -37,70 +70,207 @@ class AndroidContactActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAndroidContactBinding.inflate(layoutInflater)
setContentView(binding.root) enableEdgeToEdge()
layout = binding.ContactView
aor = intent.getStringExtra("aor")!! aor = intent.getStringExtra("aor")!!
index = intent.getIntExtra("index", 0) name = intent.getStringExtra("name")!!
textAvatarView = binding.TextAvatar val title: String = name
cardAvatarView = binding.CardAvatar val contact = Contact.androidContact(name)
cardImageAvatarView = binding.ImageAvatar
nameView = binding.Name
val contact = Contact.contacts()[index] as Contact.AndroidContact if (contact == null) {
val name = contact.name Log.e(TAG, "No Android contact found with name $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() 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() { private fun goBack() {
BaresipService.activities.remove("android contact,$index") BaresipService.activities.remove("android contact,$name")
setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java)) setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
finish() 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)
}
} }
@@ -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
}
}
@@ -121,6 +121,6 @@ object Api {
external fun module_load(module: String): Int external fun module_load(module: String): Int
external fun module_unload(module: String) external fun module_unload(module: String)
external fun create_AAudio_SessionId(): Int external fun AAudio_open_stream(): Int
external fun AAudio_close_stream()
} }
@@ -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
)
}
}
@@ -1,42 +1,77 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.os.Build import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import androidx.activity.ComponentActivity
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.setContent
import androidx.core.content.ContextCompat import androidx.activity.enableEdgeToEdge
import androidx.core.graphics.Insets import androidx.compose.foundation.background
import androidx.core.view.ViewCompat import androidx.compose.foundation.clickable
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.layout.Arrangement
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.layout.Box
import androidx.core.view.updatePadding import androidx.compose.foundation.layout.Column
import com.tutpro.baresip.databinding.ActivityAudioBinding 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.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
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.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tutpro.baresip.CustomElements.Checkbox
import com.tutpro.baresip.CustomElements.verticalScrollbar
class AudioActivity : AppCompatActivity() { class AudioActivity : ComponentActivity() {
private lateinit var binding: ActivityAudioBinding
private lateinit var micGain: EditText
private lateinit var opusBitRate: EditText
private lateinit var opusPacketLoss: EditText
private lateinit var aec: CheckBox
private lateinit var speakerPhone: CheckBox
private lateinit var audioDelay: EditText
private var save = false private var save = false
private var restart = false private var restart = false
private var callVolume = BaresipService.callVolume private var oldCallVolume = BaresipService.callVolume
private var newCallVolume = oldCallVolume
private var oldMicGain = "" private var oldMicGain = ""
private var oldAudioModules = mutableMapOf<String, Boolean>() private var newMicGain = ""
private var oldOpusBitrate = "" private var newSpeakerPhone = BaresipService.speakerPhone
private var oldOpusPacketLoss = "" private val modules = Config.variables("module")
private var oldAec = false private var newAudioModules = mutableMapOf<String, Boolean>()
private var toneCountry = BaresipService.toneCountry private var oldOpusBitrate = Config.variable("opus_bitrate")
private var newOpusBitrate = oldOpusBitrate
private var oldOpusPacketLoss = Config.variable("opus_packet_loss")
private var newOpusPacketLoss = oldOpusPacketLoss
private var newAudioDelay = BaresipService.audioDelay.toString()
private var newToneCountry = BaresipService.toneCountry
private var arrowTint = Color.Unspecified
private val onBackPressedCallback = object : OnBackPressedCallback(true) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -47,174 +82,451 @@ class AudioActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAudioBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat -> enableEdgeToEdge()
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.AudioView.updatePadding(top = systemBars.top + 56)
WindowInsetsCompat.CONSUMED
}
if (!Utils.isDarkTheme(this)) val title = String.format(getString(R.string.configuration))
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
supportActionBar?.setDisplayHomeAsUpEnabled(true)
Utils.addActivity("audio") Utils.addActivity("audio")
val callVolSpinner = binding.VolumeSpinner if (!BaresipService.agcAvailable)
val volKeys = arrayListOf("None", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
val volVals = arrayListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val curVal = callVolume
val curKey = volKeys[curVal]
volKeys.removeAt(curVal)
volVals.removeAt(curVal)
volKeys.add(0, curKey)
volVals.add(0, curVal)
val callVolAdapter = ArrayAdapter(
this, android.R.layout.simple_spinner_item,
volKeys
)
callVolAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
callVolSpinner.adapter = callVolAdapter
callVolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long
) {
callVolume = volVals[volKeys.indexOf(parent.selectedItem.toString())]
}
override fun onNothingSelected(parent: AdapterView<*>) {
}
}
if (!BaresipService.agcAvailable) {
micGain = binding.MicGain
oldMicGain = Config.variable("augain") oldMicGain = Config.variable("augain")
micGain.setText(oldMicGain)
} else {
binding.MicGainLayout.visibility = View.GONE
}
speakerPhone = binding.SpeakerPhone setContent {
speakerPhone.isChecked = BaresipService.speakerPhone AppTheme {
Surface(
val modules = Config.variables("module") modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
val audioModulesList = binding.AudioModulesList ) {
var id = 1000 AudioScreen(this, title) { goBack() }
for (module in audioModules) { }
val rl = RelativeLayout(this)
val rlParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT)
rlParams.marginStart = 16
rl.layoutParams = rlParams
val tv = TextView(this)
tv.id = id++
val tvParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT)
tvParams.addRule(RelativeLayout.ALIGN_PARENT_START)
tvParams.addRule(RelativeLayout.CENTER_VERTICAL)
tvParams.addRule(RelativeLayout.START_OF, id)
tv.layoutParams = tvParams
tv.text = String.format(getString(R.string.bullet_item), module)
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
tv.setTextColor(ContextCompat.getColor(this, R.color.colorItemText))
rl.addView(tv)
val cb = CheckBox(this)
cb.id = id++
val cbParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT)
cbParams.addRule(RelativeLayout.ALIGN_PARENT_END)
cbParams.addRule(RelativeLayout.CENTER_VERTICAL)
cb.layoutParams = cbParams
cb.gravity = Gravity.END
cb.isChecked = modules.contains("${module}.so")
oldAudioModules[module] = cb.isChecked
rl.addView(cb)
audioModulesList.addView(rl)
}
opusBitRate = binding.OpusBitRate
oldOpusBitrate = Config.variable("opus_bitrate")
opusBitRate.setText(oldOpusBitrate)
opusPacketLoss = binding.OpusPacketLoss
oldOpusPacketLoss = Config.variable("opus_packet_loss")
opusPacketLoss.setText(oldOpusPacketLoss)
aec = binding.Aec
oldAec = modules.contains("webrtc_aecm.so")
aec.isChecked = oldAec
aec.setOnClickListener {
if (!aec.isChecked)
if (!Utils.isAecSupported())
Utils.alertView(this, getString(R.string.notice),
getString(R.string.no_hw_aec))
}
audioDelay = binding.AudioDelay
audioDelay.setText("${BaresipService.audioDelay}")
val toneCountrySpinner = binding.ToneCountrySpinner
val toneCountryKeys = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us")
val toneCountryVals = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US")
val keyIx = toneCountryKeys.indexOf(toneCountry)
val keyVal = toneCountryVals.elementAt(keyIx)
toneCountryKeys.removeAt(keyIx)
toneCountryVals.removeAt(keyIx)
toneCountryKeys.add(0, toneCountry)
toneCountryVals.add(0, keyVal)
val toneCountryAdapter = ArrayAdapter(this,android.R.layout.simple_spinner_item,
toneCountryVals)
toneCountryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
toneCountrySpinner.adapter = toneCountryAdapter
toneCountrySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
toneCountry = toneCountryKeys[toneCountryVals.indexOf(parent.selectedItem.toString())]
}
override fun onNothingSelected(parent: AdapterView<*>) {
} }
} }
bindTitles()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { @Composable
fun AudioScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
super.onCreateOptionsMenu(menu) Scaffold(
val inflater = menuInflater modifier = Modifier
inflater.inflate(R.menu.check_icon, menu) .fillMaxHeight()
return true .imePadding()
.safeDrawingPadding(),
containerColor = LocalCustomColors.current.background,
topBar = { TopAppBar(title, navigateBack) },
content = { contentPadding ->
AudioContent(ctx, contentPadding)
}
)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { @OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBar(title: String, navigateBack: () -> Unit) {
androidx.compose.material3.TopAppBar(
title = {
Text(
text = title,
color = LocalCustomColors.current.light,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = LocalCustomColors.current.primary
),
navigationIcon = {
IconButton(onClick = navigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = LocalCustomColors.current.light
)
}
},
actions = {
IconButton(onClick = {
checkOnClick()
}) {
Icon(
imageVector = Icons.Filled.Check,
tint = LocalCustomColors.current.light,
contentDescription = "Check"
)
}
}
)
}
@Composable
fun AudioContent(ctx: Context, contentPadding: PaddingValues) {
arrowTint = if (BaresipService.darkTheme.value)
LocalCustomColors.current.grayLight
else
LocalCustomColors.current.black
Column(
modifier = Modifier
.fillMaxWidth()
.background(LocalCustomColors.current.background)
.padding(contentPadding),
) {
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.imePadding()
.fillMaxWidth()
.padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
.verticalScrollbar(
state = lazyListState,
width = 4.dp,
color = LocalCustomColors.current.gray
)
.background(LocalCustomColors.current.background),
state = lazyListState,
) {
item { CallVolume(ctx) }
item { MicGain(ctx) }
item { SpeakerPhone(ctx) }
item { AudioModules(ctx) }
item { OpusBitRate(ctx) }
item { OpusPacketLoss(ctx) }
item { AudioDelay(ctx) }
item { ToneCountry(ctx) }
}
}
}
@Composable
private fun CallVolume(ctx: Context) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Text(text = stringResource(R.string.default_call_volume),
modifier = Modifier
.weight(1f)
.clickable {
Utils.alertView(
ctx, getString(R.string.default_call_volume),
getString(R.string.default_call_volume_help)
)
},
color = LocalCustomColors.current.itemText,
fontSize = 18.sp)
val isDropDownExpanded = remember {
mutableStateOf(false)
}
val volNames = listOf("--", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
val volValues = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val itemPosition = remember {
mutableIntStateOf(volValues.indexOf(oldCallVolume))
}
Box {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable {
isDropDownExpanded.value = true
}
) {
Text(text = volNames[itemPosition.intValue],
color = LocalCustomColors.current.itemText)
CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
tint = arrowTint)
}
DropdownMenu(
expanded = isDropDownExpanded.value,
onDismissRequest = {
isDropDownExpanded.value = false
}) {
volNames.forEachIndexed { index, vol ->
DropdownMenuItem(text = {
Text(text = vol)
},
onClick = {
isDropDownExpanded.value = false
itemPosition.intValue = index
newCallVolume = volValues[index]
})
if (index < 10)
HorizontalDivider(
thickness = 1.dp,
color = LocalCustomColors.current.itemText
)
}
}
}
}
}
@Composable
private fun MicGain(ctx: Context) {
if (!BaresipService.agcAvailable)
Row(
Modifier.fillMaxWidth().padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
var micGain by remember { mutableStateOf(oldMicGain) }
newMicGain = micGain
OutlinedTextField(
value = micGain,
placeholder = { Text(stringResource(R.string.microphone_gain)) },
onValueChange = {
micGain = it
newMicGain = micGain
},
modifier = Modifier
.fillMaxWidth()
.clickable {
Utils.alertView(
ctx, getString(R.string.microphone_gain),
getString(R.string.microphone_gain_help)
)
},
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = 18.sp, color = LocalCustomColors.current.itemText
),
label = { Text(stringResource(R.string.microphone_gain)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
}
}
@Composable
private fun SpeakerPhone(ctx: Context) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Text(text = stringResource(R.string.speaker_phone),
modifier = Modifier
.weight(1f)
.clickable {
Utils.alertView(
ctx, getString(R.string.speaker_phone),
getString(R.string.speaker_phone_help)
)
},
color = LocalCustomColors.current.itemText,
fontSize = 18.sp)
var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) }
newSpeakerPhone = speakerPhone
Checkbox(
checked = speakerPhone,
onCheckedChange = {
speakerPhone = it
newSpeakerPhone = speakerPhone
}
)
}
}
@Composable
private fun AudioModules(ctx: Context) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
) {
Text(text = stringResource(R.string.audio_modules_title),
color = LocalCustomColors.current.itemText,
fontSize = 18.sp,
modifier = Modifier.clickable {
Utils.alertView(ctx, getString(R.string.audio_modules_title),
getString(R.string.audio_modules_help))
})
for (module in audioModules) {
Row(horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 18.dp)
) {
Text(text = String.format(getString(R.string.bullet_item), module),
color = LocalCustomColors.current.itemText,
fontSize = 18.sp)
Spacer(modifier = Modifier.weight(1f))
Checkbox(
checked = modules.contains("${module}.so"),
onCheckedChange = {
newAudioModules[module] = it
}
)
}
}
}
}
@Composable
private fun OpusBitRate(ctx: Context) {
Row(
Modifier.fillMaxWidth().padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
var opusBitrate by remember { mutableStateOf(oldOpusBitrate) }
newOpusBitrate = opusBitrate
OutlinedTextField(
value = opusBitrate,
placeholder = { Text(stringResource(R.string.opus_bit_rate)) },
onValueChange = {
opusBitrate = it
newOpusBitrate = opusBitrate
},
modifier = Modifier
.fillMaxWidth()
.clickable {
Utils.alertView(ctx, getString(R.string.opus_bit_rate),
getString(R.string.opus_bit_rate_help)
)
},
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
label = { Text(stringResource(R.string.opus_bit_rate)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
}
}
@Composable
private fun OpusPacketLoss(ctx: Context) {
Row(
Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
var opusPacketLoss by remember { mutableStateOf(oldOpusPacketLoss) }
newOpusPacketLoss = opusPacketLoss
OutlinedTextField(
value = opusPacketLoss,
placeholder = { Text(stringResource(R.string.opus_packet_loss)) },
onValueChange = {
opusPacketLoss = it
newOpusPacketLoss = opusPacketLoss
},
modifier = Modifier
.fillMaxWidth()
.clickable {
Utils.alertView(ctx, getString(R.string.opus_packet_loss),
getString(R.string.opus_packet_loss_help)
)
},
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
label = { Text(stringResource(R.string.opus_packet_loss)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
}
}
@Composable
private fun AudioDelay(ctx: Context) {
Row(
Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
var audioDelay by remember { mutableStateOf(BaresipService.audioDelay.toString()) }
newAudioDelay = audioDelay
OutlinedTextField(
value = audioDelay,
placeholder = { Text(stringResource(R.string.audio_delay)) },
onValueChange = {
audioDelay = it
newAudioDelay = audioDelay
},
modifier = Modifier
.fillMaxWidth()
.clickable {
Utils.alertView(ctx, getString(R.string.audio_delay),
getString(R.string.audio_delay_help)
)
},
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = 18.sp, color = LocalCustomColors.current.itemText),
label = { Text(stringResource(R.string.audio_delay)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
}
}
@Composable
private fun ToneCountry(ctx: Context) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Text(text = stringResource(R.string.tone_country),
modifier = Modifier
.weight(1f)
.clickable {
Utils.alertView(
ctx, getString(R.string.tone_country),
getString(R.string.tone_country_help)
)
},
color = LocalCustomColors.current.itemText,
fontSize = 18.sp)
val isDropDownExpanded = remember {
mutableStateOf(false)
}
val countryNames = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US")
val countryValues = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us")
val itemPosition = remember {
mutableIntStateOf(countryValues.indexOf(BaresipService.toneCountry))
}
Box {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable {
isDropDownExpanded.value = true
}
) {
Text(text = countryNames[itemPosition.intValue],
color = LocalCustomColors.current.itemText)
CustomElements.DrawDrawable(R.drawable.arrow_drop_down,
tint = arrowTint)
}
DropdownMenu(
expanded = isDropDownExpanded.value,
onDismissRequest = {
isDropDownExpanded.value = false
}) {
countryNames.forEachIndexed { index, name ->
DropdownMenuItem(text = {
Text(text = name)
},
onClick = {
isDropDownExpanded.value = false
itemPosition.intValue = index
newToneCountry = countryValues[index]
})
if (index < 10)
HorizontalDivider(
thickness = 1.dp,
color = LocalCustomColors.current.itemText
)
}
}
}
}
}
private fun checkOnClick() {
if (BaresipService.activities.indexOf("audio") == -1) if (BaresipService.activities.indexOf("audio") == -1)
return true return
when (item.itemId) { if (BaresipService.callVolume != newCallVolume) {
BaresipService.callVolume = newCallVolume
R.id.checkIcon -> { Config.replaceVariable("call_volume", newCallVolume.toString())
if (BaresipService.callVolume != callVolume) {
BaresipService.callVolume = callVolume
Config.replaceVariable("call_volume", callVolume.toString())
save = true save = true
} }
var gain = "1.0"
if (!BaresipService.agcAvailable) { if (!BaresipService.agcAvailable) {
gain = micGain.text.toString().trim() var gain = newMicGain.trim()
if (!gain.contains(".")) if (!gain.contains("."))
gain = "$gain.0" gain = "$gain.0"
if (gain != oldMicGain) { if (gain != oldMicGain) {
@@ -223,8 +535,7 @@ class AudioActivity : AppCompatActivity() {
this, getString(R.string.notice), this, getString(R.string.notice),
"${getString(R.string.invalid_microphone_gain)}: $gain." "${getString(R.string.invalid_microphone_gain)}: $gain."
) )
micGain.setText(oldMicGain) return
return false
} }
if (gain == "1.0") { if (gain == "1.0") {
Api.module_unload("augain") Api.module_unload("augain")
@@ -237,8 +548,7 @@ class AudioActivity : AppCompatActivity() {
this, getString(R.string.error), this, getString(R.string.error),
getString(R.string.failed_to_load_module) getString(R.string.failed_to_load_module)
) )
micGain.setText(oldMicGain) return
return false
} }
Config.addVariable("module", "augain.so") Config.addVariable("module", "augain.so")
} }
@@ -249,99 +559,75 @@ class AudioActivity : AppCompatActivity() {
} }
} }
if (speakerPhone.isChecked != BaresipService.speakerPhone) { if (newSpeakerPhone != BaresipService.speakerPhone) {
BaresipService.speakerPhone = speakerPhone.isChecked BaresipService.speakerPhone = newSpeakerPhone
Config.replaceVariable("speaker_phone", Config.replaceVariable("speaker_phone",
if (BaresipService.speakerPhone) "yes" else "no") if (BaresipService.speakerPhone) "yes" else "no")
save = true save = true
} }
var id = 1001
for (module in audioModules) { for (module in audioModules) {
val box = findViewById<CheckBox>(id++) if (newAudioModules[module] != null) {
if (box.isChecked && !oldAudioModules[module]!!) { if (newAudioModules[module]!!) {
if (!modules.contains("${module}.so")) {
if (Api.module_load("${module}.so") != 0) { if (Api.module_load("${module}.so") != 0) {
Utils.alertView(this, getString(R.string.error), Utils.alertView(
"${getString(R.string.failed_to_load_module)}: ${module}.so") this, getString(R.string.error),
return false "${getString(R.string.failed_to_load_module)}: ${module}.so"
)
return
} }
Config.addVariable("module", "${module}.so") Config.addVariable("module", "${module}.so")
save = true save = true
} }
if (!box.isChecked && oldAudioModules[module]!!) { } else if (modules.contains("${module}.so")) {
Api.module_unload("${module}.so") Api.module_unload("${module}.so")
Config.removeVariableValue("module", "${module}.so") Config.removeVariableValue("module", "${module}.so")
for (ua in BaresipService.uas) for (ua in BaresipService.uas.value)
ua.account.removeAudioCodecs(module) ua.account.removeAudioCodecs(module)
AccountsActivity.saveAccounts() AccountsActivity.saveAccounts()
save = true save = true
} }
id++ }
} }
val opusBitRate = opusBitRate.text.toString().trim() if (newOpusBitrate != oldOpusBitrate) {
if (opusBitRate != oldOpusBitrate) { if (!checkOpusBitRate(newOpusBitrate)) {
if (!checkOpusBitRate(opusBitRate)) {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(this, getString(R.string.notice),
"${getString(R.string.invalid_opus_bitrate)}: $opusBitRate.") "${getString(R.string.invalid_opus_bitrate)}: $newOpusBitrate.")
return false return
} }
Config.replaceVariable("opus_bitrate", opusBitRate) Config.replaceVariable("opus_bitrate", newOpusBitrate)
restart = true restart = true
save = true save = true
} }
val opusPacketLoss = opusPacketLoss.text.toString().trim() if (newOpusPacketLoss != oldOpusPacketLoss) {
if (opusPacketLoss != oldOpusPacketLoss) { if (!checkOpusPacketLoss(newOpusPacketLoss)) {
if (!checkOpusPacketLoss(opusPacketLoss)) {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(this, getString(R.string.notice),
"${getString(R.string.invalid_opus_packet_loss)}: $opusPacketLoss") "${getString(R.string.invalid_opus_packet_loss)}: $newOpusPacketLoss")
return false return
} }
Config.replaceVariable("opus_packet_loss", opusPacketLoss) Config.replaceVariable("opus_packet_loss", newOpusPacketLoss)
restart = true restart = true
save = true save = true
} }
if (aec.isChecked != oldAec) { val audioDelay = newAudioDelay.trim()
if (aec.isChecked) {
Config.addVariable("module", "webrtc_aecm.so")
if (gain != "1.0") {
restart = true
} else {
if (Api.module_load("webrtc_aecm.so") != 0) {
Utils.alertView(
this, getString(R.string.error),
getString(R.string.failed_to_load_module)
)
aec.isChecked = false
return false
}
}
BaresipService.webrtcAec = true
} else {
Api.module_unload("webrtc_aecm.so")
Config.removeVariableValue("module", "webrtc_aecm.so")
BaresipService.webrtcAec = false
}
save = true
}
val audioDelay = audioDelay.text.toString().trim()
if (audioDelay != BaresipService.audioDelay.toString()) { if (audioDelay != BaresipService.audioDelay.toString()) {
if (!checkAudioDelay(audioDelay)) { if (!checkAudioDelay(audioDelay)) {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.invalid_audio_delay), audioDelay)) String.format(getString(R.string.invalid_audio_delay), audioDelay))
return false return
} }
Config.replaceVariable("audio_delay", audioDelay) Config.replaceVariable("audio_delay", audioDelay)
BaresipService.audioDelay = audioDelay.toLong() BaresipService.audioDelay = audioDelay.toLong()
save = true save = true
} }
if (BaresipService.toneCountry != toneCountry) { if (BaresipService.toneCountry != newToneCountry) {
BaresipService.toneCountry = toneCountry BaresipService.toneCountry = newToneCountry
Config.replaceVariable("tone_country", toneCountry) Config.replaceVariable("tone_country", newToneCountry)
save = true save = true
} }
@@ -352,18 +638,6 @@ class AudioActivity : AppCompatActivity() {
BaresipService.activities.remove("audio") BaresipService.activities.remove("audio")
finish() finish()
return true
}
android.R.id.home -> {
goBack()
return true
}
}
return super.onOptionsItemSelected(item)
} }
private fun goBack() { private fun goBack() {
@@ -371,46 +645,6 @@ class AudioActivity : AppCompatActivity() {
finish() finish()
} }
private fun bindTitles() {
binding.VolumeTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.default_call_volume),
getString(R.string.default_call_volume_help))
}
binding.MicGainTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.microphone_gain),
getString(R.string.microphone_gain_help)
)
}
binding.SpeakerPhoneTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.speaker_phone),
getString(R.string.speaker_phone_help))
}
binding.AudioModulesTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.audio_modules_title),
getString(R.string.audio_modules_help))
}
binding.OpusBitRateTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.opus_bit_rate),
getString(R.string.opus_bit_rate_help))
}
binding.OpusPacketLossTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.opus_packet_loss),
getString(R.string.opus_packet_loss_help))
}
binding.AecTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.aec),
getString(R.string.aec_help))
}
binding.AudioDelayTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.audio_delay),
getString(R.string.audio_delay_help))
}
binding.ToneCountryTitle.setOnClickListener {
Utils.alertView(this, getString(R.string.tone_country),
getString(R.string.tone_country_help))
}
}
private fun checkMicGain(micGain: String): Boolean { private fun checkMicGain(micGain: String): Boolean {
val number = val number =
try { try {
@@ -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()
}
}
@@ -2,7 +2,11 @@ package com.tutpro.baresip
import android.Manifest import android.Manifest
import android.annotation.SuppressLint 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.BluetoothAdapter
import android.bluetooth.BluetoothHeadset import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
@@ -13,20 +17,37 @@ import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
import android.content.res.Configuration
import android.database.ContentObserver 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_IN_COMMUNICATION
import android.media.AudioManager.MODE_NORMAL import android.media.AudioManager.MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT 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.AcousticEchoCanceler
import android.media.audiofx.AutomaticGainControl import android.media.audiofx.AutomaticGainControl
import android.media.audiofx.NoiseSuppressor 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.net.wifi.WifiManager
import android.os.*
import android.os.Build.VERSION 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.ContactsContract
import android.provider.Settings import android.provider.Settings
import android.system.OsConstants
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
@@ -37,7 +58,10 @@ import android.widget.Toast
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.annotation.StringRes 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.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -49,7 +73,9 @@ import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.StandardCharsets 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.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -99,7 +125,7 @@ class BaresipService: Service() {
filesPath = filesDir.absolutePath filesPath = filesDir.absolutePath
pName = packageName pName = packageName
am = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager am = applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager
val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext, val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext,
RingtoneManager.TYPE_NOTIFICATION) RingtoneManager.TYPE_NOTIFICATION)
@@ -113,14 +139,14 @@ class BaresipService: Service() {
createNotificationChannels() createNotificationChannels()
snb = NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID) 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) { 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 vibratorManager.defaultVibrator
} else { } else {
@Suppress("DEPRECATION") @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 // 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() val builder = NetworkRequest.Builder()
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
cm.registerNetworkCallback( 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) hotSpotIsEnabled = Utils.isHotSpotOn(wm)
hotSpotReceiver = object : BroadcastReceiver() { hotSpotReceiver = object : BroadcastReceiver() {
@@ -238,9 +264,9 @@ class BaresipService: Service() {
this.registerReceiver(hotSpotReceiver, this.registerReceiver(hotSpotReceiver,
IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED")) 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 btAdapter = btm.adapter
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
@@ -530,10 +556,7 @@ class BaresipService: Service() {
if (ua == null) if (ua == null)
Log.w(TAG, "onStartCommand did not find UA $uap") Log.w(TAG, "onStartCommand did not find UA $uap")
else else
ChatsActivity.saveUaMessage( Message.updateAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
ua.account.aor,
intent.getStringExtra("time")!!.toLong()
)
nm.cancel(MESSAGE_NOTIFICATION_ID) nm.cancel(MESSAGE_NOTIFICATION_ID)
} }
@@ -543,10 +566,8 @@ class BaresipService: Service() {
if (ua == null) if (ua == null)
Log.w(TAG, "onStartCommand did not find UA $uap") Log.w(TAG, "onStartCommand did not find UA $uap")
else else
ChatsActivity.deleteUaMessage( Message.deleteAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
ua.account.aor,
intent.getStringExtra("time")!!.toLong()
)
nm.cancel(MESSAGE_NOTIFICATION_ID) nm.cancel(MESSAGE_NOTIFICATION_ID)
} }
@@ -571,6 +592,20 @@ class BaresipService: Service() {
return START_STICKY 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? { override fun onBind(intent: Intent): IBinder? {
return null return null
} }
@@ -598,7 +633,7 @@ class BaresipService: Service() {
R.drawable.circle_white R.drawable.circle_white
else else
R.drawable.circle_yellow R.drawable.circle_yellow
uas.add(ua) ua.add()
val acc = ua.account val acc = ua.account
if (acc.authUser != "" && acc.authPass == NO_AUTH_PASS) { if (acc.authUser != "" && acc.authPass == NO_AUTH_PASS) {
@@ -629,6 +664,47 @@ class BaresipService: Service() {
if (ev[0] == "recorder sessionid") { if (ev[0] == "recorder sessionid") {
recorderSessionId = ev[1].toInt() recorderSessionId = ev[1].toInt()
Log.d(TAG, "got recorder sessionid $recorderSessionId") 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 return
} }
@@ -649,31 +725,32 @@ class BaresipService: Service() {
return return
} }
for (accountIndex in uas.indices) { for (accountIndex in uas.value.indices) {
if (uas[accountIndex].account.aor == aor) { if (uas.value[accountIndex].account.aor == aor) {
when (ev[0]) { when (ev[0]) {
"registering", "unregistering" -> { "registering", "unregistering" -> {
ua.status = R.drawable.circle_yellow
updateStatusNotification() updateStatusNotification()
ua.uaUpdateStatus(R.drawable.circle_yellow)
if (isMainVisible) if (isMainVisible)
registrationUpdate.postValue(System.currentTimeMillis()) registrationUpdate.postValue(System.currentTimeMillis())
return return
} }
"registered" -> { "registered" -> {
ua.status = if (Api.account_regint(ua.account.accp) == 0) ua.uaUpdateStatus(
if (Api.account_regint(ua.account.accp) == 0)
R.drawable.circle_white R.drawable.circle_white
else else
R.drawable.circle_green R.drawable.circle_green)
updateStatusNotification() updateStatusNotification()
if (isMainVisible) if (isMainVisible)
registrationUpdate.postValue(System.currentTimeMillis()) registrationUpdate.postValue(System.currentTimeMillis())
return return
} }
"registering failed" -> { "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 R.drawable.circle_white
else else
R.drawable.circle_red R.drawable.circle_red)
updateStatusNotification() updateStatusNotification()
if (isMainVisible) if (isMainVisible)
registrationUpdate.postValue(System.currentTimeMillis()) registrationUpdate.postValue(System.currentTimeMillis())
@@ -752,7 +829,7 @@ class BaresipService: Service() {
"call incoming" -> { "call incoming" -> {
val peerUri = ev[1] val peerUri = ev[1]
Log.d(TAG, "Incoming call $uap/$callp/$peerUri") 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)) if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am) Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
if (ua.account.answerMode == Api.ANSWERMODE_MANUAL) { if (ua.account.answerMode == Api.ANSWERMODE_MANUAL) {
@@ -827,47 +904,6 @@ class BaresipService: Service() {
if (am.mode != MODE_IN_COMMUNICATION) if (am.mode != MODE_IN_COMMUNICATION)
am.mode = MODE_IN_COMMUNICATION am.mode = MODE_IN_COMMUNICATION
call!!.status = "connected" 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 call.onhold = false
if (ua.account.callHistory) if (ua.account.callHistory)
call.startTime = GregorianCalendar() call.startTime = GregorianCalendar()
@@ -951,8 +987,11 @@ class BaresipService: Service() {
stopRinging() stopRinging()
stopMediaPlayer() stopMediaPlayer()
aec?.release() aec?.release()
aec = null
agc?.release() agc?.release()
agc = null
ns?.release() ns?.release()
ns = null
val newCall = call.newCall val newCall = call.newCall
if (newCall != null) { if (newCall != null) {
newCall.onHoldCall = null newCall.onHoldCall = null
@@ -1237,14 +1276,14 @@ class BaresipService: Service() {
2-> R.id.status2 2-> R.id.status2
else -> R.id.status3 else -> R.id.status3
} }
if (i < uas.size) { if (i < uas.value.size) {
notificationLayout.setImageViewResource(resId, uas[i].status) notificationLayout.setImageViewResource(resId, uas.value[i].status)
notificationLayout.setViewVisibility(resId, View.VISIBLE) notificationLayout.setViewVisibility(resId, View.VISIBLE)
} else { } else {
notificationLayout.setViewVisibility(resId, View.INVISIBLE) notificationLayout.setViewVisibility(resId, View.INVISIBLE)
} }
} }
if (uas.size > 4) if (uas.value.size > 4)
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE) notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
else else
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE) notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
@@ -1467,7 +1506,7 @@ class BaresipService: Service() {
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) { if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) {
val props = cm.getLinkProperties(n) ?: continue val props = cm.getLinkProperties(n) ?: continue
for (la in props.linkAddresses) 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 && props.interfaceName != null && la.address.hostAddress != null &&
afMatch(la.address.hostAddress!!)) afMatch(la.address.hostAddress!!))
addresses[la.address.hostAddress!!] = props.interfaceName!! addresses[la.address.hostAddress!!] = props.interfaceName!!
@@ -1544,9 +1583,10 @@ class BaresipService: Service() {
stopRinging() stopRinging()
stopMediaPlayer() stopMediaPlayer()
abandonAudioFocus(applicationContext) abandonAudioFocus(applicationContext)
uas.clear() uas.value = emptyList()
uasStatus.value = emptyMap()
callHistory.clear() callHistory.clear()
messages.clear() messages = emptyList()
if (this::nm.isInitialized) if (this::nm.isInitialized)
nm.cancelAll() nm.cancelAll()
if (this::partialWakeLock.isInitialized && partialWakeLock.isHeld) if (this::partialWakeLock.isInitialized && partialWakeLock.isHeld)
@@ -1590,17 +1630,20 @@ class BaresipService: Service() {
var isRecOn = false var isRecOn = false
var toneCountry = "us" 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>() val calls = ArrayList<Call>()
var callHistory = ArrayList<CallHistoryNew>() var callHistory = ArrayList<CallHistoryNew>()
var messages = ArrayList<Message>()
val messageUpdate = MutableLiveData<Long>() val messageUpdate = MutableLiveData<Long>()
val contactUpdate = MutableLiveData<Long>() val contactUpdate = MutableLiveData<Long>()
val registrationUpdate = MutableLiveData<Long>() val registrationUpdate = MutableLiveData<Long>()
val baresipContacts = ArrayList<Contact.BaresipContact>() val baresipContacts = mutableStateOf(emptyList<Contact.BaresipContact>())
val androidContacts = ArrayList<Contact.AndroidContact>() val androidContacts = mutableStateOf(emptyList<Contact.AndroidContact>())
val contacts = ArrayList<Contact>() val contactNames = mutableStateOf(emptyList<String>())
val contactNames = ArrayList<String>()
var contactsMode = "baresip" var contactsMode = "baresip"
val chatTexts: MutableMap<String, String> = mutableMapOf() val chatTexts: MutableMap<String, String> = mutableMapOf()
val activities = mutableListOf<String>() val activities = mutableListOf<String>()
@@ -1611,11 +1654,12 @@ class BaresipService: Service() {
// <aor, password> of those accounts that have auth username without auth password // <aor, password> of those accounts that have auth username without auth password
val aorPasswords = mutableMapOf<String, String>() val aorPasswords = mutableMapOf<String, String>()
var audioFocusRequest: AudioFocusRequestCompat? = null var audioFocusRequest: AudioFocusRequestCompat? = null
var agcAvailable = AutomaticGainControl.isAvailable() var aecAvailable = false
private val nsAvailable = NoiseSuppressor.isAvailable()
private var aec: AcousticEchoCanceler? = null private var aec: AcousticEchoCanceler? = null
private var ns: NoiseSuppressor? = null var agcAvailable = false
private var agc: AutomaticGainControl? = null private var agc: AutomaticGainControl? = null
private val nsAvailable = NoiseSuppressor.isAvailable()
private var ns: NoiseSuppressor? = null
var webrtcAec = false var webrtcAec = false
private var btAdapter: BluetoothAdapter? = null private var btAdapter: BluetoothAdapter? = null
private var recorderSessionId = 0 private var recorderSessionId = 0
@@ -1626,7 +1670,7 @@ class BaresipService: Service() {
Log.d(TAG, "Already focused") Log.d(TAG, "Already focused")
return true return true
} }
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
val attributes = AudioAttributesCompat.Builder() val attributes = AudioAttributesCompat.Builder()
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION) .setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH) .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
@@ -1647,7 +1691,7 @@ class BaresipService: Service() {
} }
fun abandonAudioFocus(ctx: Context) { fun abandonAudioFocus(ctx: Context) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
if (audioFocusRequest != null) { if (audioFocusRequest != null) {
Log.d(TAG, "Abandoning audio focus") Log.d(TAG, "Abandoning audio focus")
if (AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) == if (AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) ==
@@ -1683,7 +1727,7 @@ class BaresipService: Service() {
} }
private fun startBluetoothSco(ctx: Context, delay: Long, count: Int) { 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)) { if (isBluetoothScoOn(am)) {
Log.d(TAG, "Bluetooth SCO is already on") Log.d(TAG, "Bluetooth SCO is already on")
return return
@@ -1705,7 +1749,7 @@ class BaresipService: Service() {
private fun stopBluetoothSco(ctx: Context) { private fun stopBluetoothSco(ctx: Context) {
Log.d(TAG, "Stopping Bluetooth SCO") 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)) { if (!isBluetoothScoOn(am)) {
Log.d(TAG, "Bluetooth SCO is already off") Log.d(TAG, "Bluetooth SCO is already off")
return return
@@ -1719,7 +1763,5 @@ class BaresipService: Service() {
am.isBluetoothScoOn = false am.isBluetoothScoOn = false
}, 100) }, 100)
} }
} }
} }
@@ -1,24 +1,67 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer import android.media.MediaPlayer
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils
import android.view.MenuItem import android.view.MenuItem
import android.view.View import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.core.graphics.Insets import androidx.activity.compose.setContent
import androidx.core.view.ViewCompat import androidx.activity.enableEdgeToEdge
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.Image
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.background
import androidx.core.view.updatePadding import androidx.compose.foundation.clickable
import com.tutpro.baresip.databinding.ActivityCallDetailsBinding 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 aor: String
private lateinit var peer: 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 decPlayer = MediaPlayer()
private val encPlayer = MediaPlayer() private val encPlayer = MediaPlayer()
private var position = 0 private var position = 0
@@ -33,38 +76,282 @@ class CallDetailsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) 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")!! aor = intent.getStringExtra("aor")!!
account = Account.ofAor(aor)!!
peer = intent.getStringExtra("peer")!! peer = intent.getStringExtra("peer")!!
position = intent.getIntExtra("position", 0) position = intent.getIntExtra("position", 0)
callRow = uaHistory.value[position]
details = callRow.details
Utils.addActivity("call_details,$aor,$peer,$position") Utils.addActivity("call_details,$aor,$peer,$position")
val headerView = binding.peer setContent {
val headerText = "${getString(R.string.peer)} $peer" AppTheme {
headerView.text = headerText Surface(
modifier = Modifier.fillMaxSize(),
val listView = binding.calls color = LocalCustomColors.current.background
listView.adapter = CallDetailsAdapter(this, CallsActivity.uaHistory[position].details, ) {
decPlayer, encPlayer) CallDetailsScreen(this, getString(R.string.call_details)) { goBack() }
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) 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() { override fun onPause() {
MainActivity.activityAor = aor MainActivity.activityAor = aor
super.onPause() super.onPause()
@@ -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
}
}
@@ -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
}
}
@@ -1,31 +1,75 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Build import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import androidx.activity.ComponentActivity
import android.widget.AdapterView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.activity.enableEdgeToEdge
import androidx.core.graphics.Insets import androidx.compose.foundation.Image
import androidx.core.view.ViewCompat import androidx.compose.foundation.background
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.clickable
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.combinedClickable
import androidx.core.view.updatePadding 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.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 account: Account
private lateinit var clAdapter: CallListAdapter
private var aor = "" private var aor = ""
private var lastClick: Long = 0 private var lastClick: Long = 0
@@ -40,43 +84,282 @@ class CallsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityCallsBinding.inflate(layoutInflater) enableEdgeToEdge()
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat -> val title = String.format(getString(R.string.call_history))
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
aor = intent.getStringExtra("aor")!! aor = intent.getStringExtra("aor")!!
Utils.addActivity("calls,$aor") Utils.addActivity("calls,$aor")
val ua = UserAgent.ofAor(aor)!! val ua = UserAgent.ofAor(aor)!!
account = ua.account account = ua.account
aorGenerateHistory(aor)
val headerView = binding.account setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
CallsScreen(this, title) { goBack() }
}
}
}
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) + " " + val headerText = getString(R.string.account) + " " +
if (account.nickName != "") if (account.nickName.value != "")
account.nickName account.nickName.value
else else
aor.split(":")[1] aor.split(":")[1]
headerView.text = headerText Text(
text = headerText,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = LocalCustomColors.current.itemText,
textAlign = TextAlign.Center
)
}
val listView = binding.calls @Composable
aorGenerateHistory(aor) fun Calls(ctx: Context, account: Account) {
clAdapter = CallListAdapter(this, account, uaHistory) val lazyListState = rememberLazyListState()
listView.adapter = clAdapter LazyColumn(
listView.isLongClickable = true 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 ->
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ -> var recordings = false
val peerUri = uaHistory[pos].peerUri
val peerName = Utils.friendlyUri(this, peerUri, account) 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)
})
)
}
}
}
}
}
private fun callMessageClick(ctx: Context, callRow: CallRow) {
val peerUri = callRow.peerUri
val peerName = Utils.friendlyUri(ctx, peerUri, account)
val dialogClickListener = DialogInterface.OnClickListener { _, which -> val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) { when (which) {
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> { DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
@@ -89,7 +372,7 @@ class CallsActivity : AppCompatActivity() {
i.putExtra("action", "call") i.putExtra("action", "call")
else else
i.putExtra("action", "message") i.putExtra("action", "message")
i.putExtra("uap", ua.uap) i.putExtra("uap", UserAgent.ofAor(aor)!!.uap)
i.putExtra("peer", peerUri) i.putExtra("peer", peerUri)
startActivity(i) startActivity(i)
} }
@@ -100,59 +383,62 @@ class CallsActivity : AppCompatActivity() {
} }
if (SystemClock.elapsedRealtime() - lastClick > 1000) { if (SystemClock.elapsedRealtime() - lastClick > 1000) {
lastClick = SystemClock.elapsedRealtime() lastClick = SystemClock.elapsedRealtime()
with(MaterialAlertDialogBuilder(this@CallsActivity, R.style.AlertDialogTheme)) { with(
MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
) {
setTitle(R.string.confirmation) setTitle(R.string.confirmation)
setMessage( setMessage(
String.format( String.format(ctx.getString(R.string.contact_action_question), peerName)
getString(R.string.calls_call_message_question),
peerName
) )
setNeutralButton(
ctx.getText(R.string.cancel),
dialogClickListener
)
setNegativeButton(
ctx.getText(R.string.call),
dialogClickListener
)
setPositiveButton(
ctx.getText(R.string.send_message),
dialogClickListener
) )
setNeutralButton(getString(R.string.cancel), dialogClickListener)
setNegativeButton(getString(R.string.call), dialogClickListener)
setPositiveButton(getString(R.string.send_message), dialogClickListener)
show() show()
} }
} }
} }
val contactRequest = private fun callMessageLongClick(ctx: Context, callRow: CallRow) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { val peerUri = callRow.peerUri
if (it.resultCode == RESULT_OK) { val peerName = Utils.friendlyUri(ctx, peerUri, account)
clAdapter.notifyDataSetChanged() val dialogClickListener =
} DialogInterface.OnClickListener { _, which ->
}
listView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, pos, _ ->
val peerUri = uaHistory[pos].peerUri
val peerName = Utils.friendlyUri(this, peerUri, account)
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) { when (which) {
DialogInterface.BUTTON_NEGATIVE -> { DialogInterface.BUTTON_NEGATIVE -> {
val i = Intent(this, ContactActivity::class.java) val i = Intent(ctx, BaresipContactActivity::class.java)
val b = Bundle() val b = Bundle()
b.putBoolean("new", true) b.putBoolean("new", true)
b.putString("uri", peerUri) b.putString("uri", peerUri)
i.putExtras(b) i.putExtras(b)
contactRequest.launch(i) ctx.startActivity(i)
} }
DialogInterface.BUTTON_POSITIVE -> { DialogInterface.BUTTON_POSITIVE -> {
removeUaHistoryAt(pos) removeFromHistory(callRow)
CallHistoryNew.save() CallHistoryNew.save()
clAdapter.notifyDataSetChanged() /// clAdapter.notifyDataSetChanged()
} }
DialogInterface.BUTTON_NEUTRAL -> { DialogInterface.BUTTON_NEUTRAL -> {
} }
} }
} }
val callText: String = if (uaHistory[pos].details.size > 1) val callText: String = if (callRow.details.size > 1)
getString(R.string.calls_calls) getString(R.string.calls_calls)
else else
getString(R.string.calls_call) getString(R.string.calls_call)
val builder = MaterialAlertDialogBuilder(this@CallsActivity, R.style.AlertDialogTheme) val builder = MaterialAlertDialogBuilder(
if (!Contact.nameExists(peerName, false)) this@CallsActivity,
R.style.AlertDialogTheme
)
if (!Contact.nameExists(peerName, BaresipService.contacts, false))
with(builder) { with(builder) {
setTitle(R.string.confirmation) setTitle(R.string.confirmation)
setMessage( setMessage(
@@ -161,9 +447,18 @@ class CallsActivity : AppCompatActivity() {
peerName, callText peerName, callText
) )
) )
setNeutralButton(getString(R.string.cancel), dialogClickListener) setNeutralButton(
setPositiveButton(getString(R.string.delete), dialogClickListener) getString(R.string.cancel),
setNegativeButton(getString(R.string.add_contact), dialogClickListener) dialogClickListener
)
setPositiveButton(
getString(R.string.delete),
dialogClickListener
)
setNegativeButton(
getString(R.string.add_contact),
dialogClickListener
)
show() show()
} }
else else
@@ -175,18 +470,16 @@ class CallsActivity : AppCompatActivity() {
peerName, callText peerName, callText
) )
) )
setNeutralButton(getString(R.string.cancel), dialogClickListener) setNeutralButton(
setPositiveButton(getString(R.string.delete), dialogClickListener) getString(R.string.cancel),
dialogClickListener
)
setPositiveButton(
getString(R.string.delete),
dialogClickListener
)
show() show()
} }
true
}
ua.account.missedCalls = false
invalidateOptionsMenu()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -205,7 +498,7 @@ class CallsActivity : AppCompatActivity() {
CallHistoryNew.clear(aor) CallHistoryNew.clear(aor)
CallHistoryNew.save() CallHistoryNew.save()
aorGenerateHistory(aor) aorGenerateHistory(aor)
clAdapter.notifyDataSetChanged() // clAdapter.notifyDataSetChanged()
dialog.dismiss() dialog.dismiss()
} }
setNeutralButton(getText(R.string.cancel)) { dialog, _ -> setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
@@ -243,7 +536,7 @@ class CallsActivity : AppCompatActivity() {
} }
private fun returnResult() { private fun returnResult() {
setResult(Activity.RESULT_CANCELED, Intent()) setResult(RESULT_CANCELED, Intent())
finish() finish()
} }
@@ -263,7 +556,7 @@ class CallsActivity : AppCompatActivity() {
} }
private fun aorGenerateHistory(aor: String) { private fun aorGenerateHistory(aor: String) {
uaHistory.clear() uaHistory.value = emptyList()
for (i in BaresipService.callHistory.indices.reversed()) { for (i in BaresipService.callHistory.indices.reversed()) {
val h = BaresipService.callHistory[i] val h = BaresipService.callHistory[i]
if (h.aor == aor) { if (h.aor == aor) {
@@ -286,34 +579,54 @@ class CallsActivity : AppCompatActivity() {
R.drawable.call_missed_out R.drawable.call_missed_out
} }
} }
if (uaHistory.isNotEmpty() && (uaHistory.last().peerUri == h.peerUri)) if (uaHistory.value.isNotEmpty() && (uaHistory.value.last().peerUri == h.peerUri))
uaHistory.last().details.add(CallRow.Details( uaHistory.value.last().details.add(CallRow.Details(
direction, h.startTime, direction, h.startTime,
h.stopTime, h.recording h.stopTime, h.recording
)) ))
else else
uaHistory.add(CallRow( addToUaHistory(
h.aor, h.peerUri, direction, h.startTime, h.stopTime, CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording)
h.recording )
))
} }
} }
} }
private fun removeUaHistoryAt(i: Int) { private fun removeFromHistory(callRow: CallRow) {
for (details in uaHistory[i].details) { for (details in callRow.details) {
if (details.recording[0] != "") if (details.recording[0] != "")
CallHistoryNew.deleteRecording(details.recording) CallHistoryNew.deleteRecording(details.recording)
BaresipService.callHistory.removeAll { BaresipService.callHistory.removeAll {
it.startTime == details.startTime && it.stopTime == details.stopTime it.startTime == details.startTime && it.stopTime == details.stopTime
} }
} }
CallHistoryNew.deleteRecording(uaHistory[i].recording) CallHistoryNew.deleteRecording(callRow.recording)
uaHistory.removeAt(i) 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 { companion object {
var uaHistory = ArrayList<CallRow>() val uaHistory = mutableStateOf(emptyList<CallRow>())
} }
} }
@@ -1,45 +1,103 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.text.format.DateUtils.isToday
import android.view.Menu import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ListView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.setContent
import androidx.core.graphics.Insets import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.Image
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.background
import androidx.core.view.updatePadding 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 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 imm: InputMethodManager
private lateinit var aor: String private lateinit var aor: String
private lateinit var account: Account
private lateinit var peerUri: String private lateinit var peerUri: String
private lateinit var chatPeer: String
private lateinit var ua: UserAgent 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 focus = false
private var lastCall: Long = 0 private var lastCall: Long = 0
private var unsentMessage = ""
private var keyboardController: SoftwareKeyboardController? = null
private val onBackPressedCallback = object : OnBackPressedCallback(true) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -51,28 +109,15 @@ class ChatActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityChatBinding.inflate(layoutInflater) enableEdgeToEdge()
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)
aor = intent.getStringExtra("aor")!! aor = intent.getStringExtra("aor")!!
peerUri = intent.getStringExtra("peer")!! peerUri = intent.getStringExtra("peer")!!
account = UserAgent.ofAor(aor)!!.account
focus = intent.getBooleanExtra("focus", false) focus = intent.getBooleanExtra("focus", false)
if (BaresipService.activities.first().startsWith("chat,$aor,$peerUri")) { if (BaresipService.activities.first().startsWith("chat,$aor,$peerUri")) {
returnResult(Activity.RESULT_CANCELED) returnResult(RESULT_CANCELED)
return return
} else { } else {
Utils.addActivity("chat,$aor,$peerUri,$focus") Utils.addActivity("chat,$aor,$peerUri,$focus")
@@ -82,92 +127,474 @@ class ChatActivity : AppCompatActivity() {
if (userAgent == null) { if (userAgent == null) {
Log.w(TAG, "MessageActivity did not find ua of $aor") Log.w(TAG, "MessageActivity did not find ua of $aor")
MainActivity.activityAor = aor MainActivity.activityAor = aor
returnResult(Activity.RESULT_CANCELED) returnResult(RESULT_CANCELED)
return return
} else { } else {
ua = userAgent ua = userAgent
} }
chatPeer = Utils.friendlyUri(this, peerUri, userAgent.account, true)
imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 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) val title = String.format(getString(R.string.chat_with), chatPeer)
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 messagesObserver = Observer<Long> { val messagesObserver = Observer<Long> {
chatMessages.clear() _chatMessages.value = listOf()
chatMessages.addAll(uaPeerMessages(aor, peerUri)) _chatMessages.value = uaPeerMessages(aor, peerUri)
mlAdapter.notifyDataSetChanged()
} }
BaresipService.messageUpdate.observe(this, messagesObserver) BaresipService.messageUpdate.observe(this, messagesObserver)
listView.adapter = mlAdapter ua.account.unreadMessages = false
//listView.isLongClickable = true
val footerView = View(applicationContext)
listView.addFooterView(footerView)
listView.smoothScrollToPosition(mlAdapter.count - 1)
newMessage = binding.text setContent {
newMessage.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> AppTheme {
if (hasFocus) { keyboardController = LocalSoftwareKeyboardController.current
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) Surface(
modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
) {
ChatScreen(this, title) { goBack() }
}
} }
} }
if (focus) newMessage.requestFocus() onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
sendButton = binding.sendButton @Composable
sendButton.setOnClickListener { fun ChatScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
val msgText = newMessage.text.toString() 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()) { if (msgText.isNotEmpty()) {
imm.hideSoftInputFromWindow(newMessage.windowToken, 0) keyboardController?.hide()
val time = System.currentTimeMillis() val time = System.currentTimeMillis()
val msg = Message(aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true) val msg = Message(
aor,
peerUri,
msgText,
time,
MESSAGE_UP_WAIT,
0,
"",
true
)
msg.add() msg.add()
var msgUri = "" var msgUri = ""
chatMessages.add(msg) _chatMessages.value += msg
if (Utils.isTelUri(peerUri)) if (Utils.isTelUri(peerUri))
if (ua.account.telProvider == "") { if (ua.account.telProvider == "") {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(
String.format(getString(R.string.no_telephony_provider), ctx, getString(R.string.notice),
Utils.plainAor(aor))) String.format(
getString(R.string.no_telephony_provider),
Utils.plainAor(aor)
)
)
} else { } else {
msgUri = Utils.telToSip(peerUri, ua.account) msgUri = Utils.telToSip(peerUri, ua.account)
} }
else else
msgUri = peerUri msgUri = peerUri
if (msgUri != "") if (msgUri != "")
if (Api.message_send(ua.uap, msgUri, msgText, time.toString()) != 0) { if (Api.message_send(
Toast.makeText(applicationContext, "${getString(R.string.message_failed)}!", ua.uap,
Toast.LENGTH_SHORT).show() msgUri,
msgText,
time.toString()
) != 0
) {
Toast.makeText(
ctx,
"${getString(R.string.message_failed)}!",
Toast.LENGTH_SHORT
).show()
msg.direction = MESSAGE_UP_FAIL msg.direction = MESSAGE_UP_FAIL
msg.responseReason = getString(R.string.message_failed) msg.responseReason = getString(R.string.message_failed)
} else { } else {
newMessage.setText("") newMessage = ""
unsentMessage = ""
keyboardController?.hide()
BaresipService.chatTexts.remove("$aor::$peerUri") BaresipService.chatTexts.remove("$aor::$peerUri")
} }
mlAdapter.notifyDataSetChanged() }
},
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")
} }
} }
}
ua.account.unreadMessages = false )
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -179,61 +606,24 @@ class ChatActivity : AppCompatActivity() {
super.onResume() super.onResume()
val chatText = BaresipService.chatTexts["$aor::$peerUri"] val chatText = BaresipService.chatTexts["$aor::$peerUri"]
if (chatText != null) { if (chatText != null) {
Log.d(TAG, "Restoring newMessage ${newMessage.text} for $aor::$peerUri") Log.d(TAG, "Restoring newMessage $chatText for $aor::$peerUri")
newMessage.setText(chatText) unsentMessage = chatText
newMessage.requestFocus() //newMessage.requestFocus()
BaresipService.chatTexts.remove("$aor::$peerUri") BaresipService.chatTexts.remove("$aor::$peerUri")
} }
chatMessages.clear() _chatMessages.value = uaPeerMessages(aor, peerUri)
chatMessages.addAll(uaPeerMessages(aor, peerUri))
mlAdapter.notifyDataSetChanged()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
if (newMessage.text.toString() != "") { if (unsentMessage != "") {
Log.d(TAG, "Saving newMessage ${newMessage.text} for $aor::$peerUri") Log.d(TAG, "Saving newMessage $unsentMessage for $aor::$peerUri")
BaresipService.chatTexts["$aor::$peerUri"] = newMessage.text.toString() BaresipService.chatTexts["$aor::$peerUri"] = unsentMessage
} }
MainActivity.activityAor = aor 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() { private fun goBack() {
var save = false var save = false
for (m in chatMessages) { for (m in chatMessages) {
if (m.new) { if (m.new) {
@@ -242,13 +632,10 @@ class ChatActivity : AppCompatActivity() {
} }
} }
if (save) Message.save() if (save) Message.save()
keyboardController?.hide()
imm.hideSoftInputFromWindow(newMessage.windowToken, 0)
BaresipService.activities.remove("chat,$aor,$peerUri,false") BaresipService.activities.remove("chat,$aor,$peerUri,false")
BaresipService.activities.remove("chat,$aor,$peerUri,true") BaresipService.activities.remove("chat,$aor,$peerUri,true")
returnResult(Activity.RESULT_OK) returnResult(RESULT_OK)
} }
private fun returnResult(code: Int) { private fun returnResult(code: Int) {
@@ -256,9 +643,9 @@ class ChatActivity : AppCompatActivity() {
finish() finish()
} }
private fun uaPeerMessages(aor: String, peerUri: String): ArrayList<Message> { private fun uaPeerMessages(aor: String, peerUri: String): List<Message> {
val res = ArrayList<Message>() val res = mutableListOf<Message>()
for (m in Message.messages()) for (m in BaresipService.messages.reversed())
if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m) if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m)
return res return res
} }
@@ -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
}
}
@@ -1,38 +1,98 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Build import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils.isToday
import android.view.Menu import android.view.Menu
import android.view.MenuItem import androidx.activity.ComponentActivity
import android.view.View
import android.widget.*
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.animateContentSize
import androidx.core.graphics.Insets import androidx.compose.foundation.background
import androidx.core.view.ViewCompat import androidx.compose.foundation.clickable
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.combinedClickable
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.layout.Arrangement
import androidx.core.view.updatePadding 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.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 aor: String
internal lateinit var account: Account internal lateinit var account: Account
private lateinit var chatRequest: ActivityResultLauncher<Intent> 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) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -44,172 +104,536 @@ class ChatsActivity: AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityChatsBinding.inflate(layoutInflater) enableEdgeToEdge()
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
aor = intent.extras!!.getString("aor")!! aor = intent.extras!!.getString("aor")!!
Utils.addActivity("chats,$aor") Utils.addActivity("chats,$aor")
account = UserAgent.ofAor(aor)!!.account account = UserAgent.ofAor(aor)!!.account
val headerView = binding.account val title = getString(R.string.chats)
val headerText = getString(R.string.account) + " " +
if (account.nickName != "")
account.nickName
else
aor.split(":")[1]
headerView.text = headerText
uaMessages = uaMessages(aor) _uaMessages.value = uaMessages(aor)
clAdapter = ChatListAdapter(this, account, uaMessages)
listView.adapter = clAdapter
listView.isLongClickable = true
chatRequest = chatRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK)
clAdapter.clear() _uaMessages.value = uaMessages(aor)
uaMessages = uaMessages(aor)
clAdapter = ChatListAdapter(this, account, uaMessages)
listView.adapter = clAdapter
}
} }
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ -> setContent {
scrollPosition = pos AppTheme {
val i = Intent(this, ChatActivity::class.java) Surface(
val b = Bundle() modifier = Modifier.fillMaxSize(),
b.putString("aor", aor) color = LocalCustomColors.current.background
b.putString("peer", uaMessages[pos].peerUri) ) {
i.putExtras(b) ChatsScreen(this, title) { goBack() }
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 -> {
} }
} }
} }
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) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
} }
private fun makeChat(lookForContact: Boolean = true) { @Composable
var uriText = peerUri.text.toString().trim() fun ChatsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
if (uriText.isNotEmpty()) { 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) { if (lookForContact) {
val uris = Contact.contactUris(uriText) val uris = Contact.contactUris(peerUri)
if (uris.size == 1) if (uris.size == 1)
uriText = uris[0] peerUri = uris[0]
else if (uris.size > 1) { else if (uris.size > 1) {
val builder = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme) val builder = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
with(builder) { with(builder) {
setTitle("Choose Destination") setTitle("Choose Destination")
setItems(uris.toTypedArray()) { _, which -> setItems(uris.toTypedArray()) { _, which ->
peerUri.setText(uris[which]) peerUri = uris[which]
makeChat(false) makeChat(peerUri, false)
} }
setNeutralButton(getString(R.string.cancel)) { _: DialogInterface, _: Int -> } setNeutralButton(getString(R.string.cancel)) { _: DialogInterface, _: Int -> }
show() show()
} }
return return true
} }
} }
if (Utils.isTelNumber(uriText)) if (Utils.isTelNumber(peerUri))
uriText = "tel:$uriText" peerUri = "tel:$peerUri"
val uri = if (Utils.isTelUri(uriText)) { val uri = if (Utils.isTelUri(peerUri)) {
if (account.telProvider == "") { if (account.telProvider == "") {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.no_telephony_provider), account.aor)) String.format(getString(R.string.no_telephony_provider), account.aor))
return return false
} }
Utils.telToSip(uriText, account) Utils.telToSip(peerUri, account)
} else { } else {
Utils.uriComplete(uriText, aor) Utils.uriComplete(peerUri, aor)
} }
if (!Utils.checkUri(uri)) { if (!Utils.checkUri(uri)) {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.invalid_sip_or_tel_uri), uri)) String.format(getString(R.string.invalid_sip_or_tel_uri), uri))
return false
} else { } else {
peerUri.text.clear()
peerUri.isCursorVisible = false
val i = Intent(this@ChatsActivity, ChatActivity::class.java) val i = Intent(this@ChatsActivity, ChatActivity::class.java)
val b = Bundle() val b = Bundle()
b.putString("aor", aor) b.putString("aor", aor)
b.putString("peer", uri) b.putString("peer", uri)
i.putExtras(b) i.putExtras(b)
chatRequest.launch(i) chatRequest.launch(i)
return true
} }
} }
return false
} }
override fun onPause() { override fun onPause() {
@@ -219,18 +643,7 @@ class ChatsActivity: AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
clAdapter.clear() _uaMessages.value = uaMessages(aor)
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)
}
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -238,55 +651,20 @@ class ChatsActivity: AppCompatActivity() {
return true 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() { private fun goBack() {
BaresipService.activities.remove("chats,$aor") BaresipService.activities.remove("chats,$aor")
returnResult() returnResult()
} }
private fun returnResult() { private fun returnResult() {
setResult(Activity.RESULT_CANCELED, Intent()) setResult(RESULT_CANCELED, Intent())
finish() finish()
} }
private fun uaMessages(aor: String) : ArrayList<Message> { private fun uaMessages(aor: String) : List<Message> {
val res = ArrayList<Message>() val res = mutableListOf<Message>()
account.unreadMessages = false account.unreadMessages = false
for (m in Message.messages().reversed()) { for (m in BaresipService.messages) {
if (m.aor != aor) continue if (m.aor != aor) continue
var found = false var found = false
for (r in res) for (r in res)
@@ -300,30 +678,6 @@ class ChatsActivity: AppCompatActivity() {
account.unreadMessages = true account.unreadMessages = true
} }
} }
return res return res.toList()
}
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
}
}
} }
} }
@@ -1,3 +1,5 @@
package com.tutpro.baresip 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>)
@@ -1,35 +1,64 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import androidx.activity.ComponentActivity
import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.setContent
import androidx.core.graphics.Insets import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.background
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.clickable
import androidx.core.view.updatePadding import androidx.compose.foundation.combinedClickable
import androidx.recyclerview.widget.ItemTouchHelper import androidx.compose.foundation.layout.Column
import androidx.recyclerview.widget.ItemTouchHelper.* import androidx.compose.foundation.layout.PaddingValues
import androidx.recyclerview.widget.LinearLayoutManager import androidx.compose.foundation.layout.fillMaxHeight
import androidx.recyclerview.widget.RecyclerView import androidx.compose.foundation.layout.fillMaxSize
import com.tutpro.baresip.databinding.ActivityCodecsBinding import androidx.compose.foundation.layout.fillMaxWidth
import java.util.* 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 acc: Account
private lateinit var ua: UserAgent 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 aor = ""
private var newCodecs = ArrayList<Codec>()
private var media = "" private var media = ""
private var title = ""
private val onBackPressedCallback = object : OnBackPressedCallback(true) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -41,21 +70,7 @@ class CodecsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityCodecsBinding.inflate(layoutInflater) enableEdgeToEdge()
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)
aor = intent.getStringExtra("aor")!! aor = intent.getStringExtra("aor")!!
media = intent.getStringExtra("media")!! media = intent.getStringExtra("media")!!
@@ -65,120 +80,231 @@ class CodecsActivity : AppCompatActivity() {
ua = UserAgent.ofAor(aor)!! ua = UserAgent.ofAor(aor)!!
acc = ua.account 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") { if (media == "audio") {
codecsTitle.text = getString(R.string.audio_codecs) title = getString(R.string.audio_codecs)
allCodecs = ArrayList(Api.audio_codecs().split(",")) allCodecs = ArrayList(Api.audio_codecs().split(","))
accCodecs = acc.audioCodec accCodecs = acc.audioCodec
} else { } else {
codecsTitle.text = getString(R.string.video_codecs) title = getString(R.string.video_codecs)
allCodecs = ArrayList(Api.video_codecs().split(",").distinct()) allCodecs = ArrayList(Api.video_codecs().split(",").distinct())
accCodecs = acc.videoCodec accCodecs = acc.videoCodec
} }
val currentCodecs = mutableListOf<Codec>()
for (codec in accCodecs) for (codec in accCodecs)
newCodecs.add(Codec(codec, true)) currentCodecs.add(Codec(codec, mutableStateOf(true)))
for (codec in allCodecs) for (codec in allCodecs)
if (codec !in accCodecs) 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) setContent {
codecList.adapter = codecsAdapter AppTheme {
Surface(
itemTouchHelper.attachToRecyclerView(binding.CodecList) modifier = Modifier.fillMaxSize(),
color = LocalCustomColors.current.background
codecsTitle.setOnClickListener { ) {
if (media == "audio") CodecsScreen { goBack() }
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))
} }
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
} }
private val itemTouchHelper by lazy { @OptIn(ExperimentalMaterial3Api::class)
val simpleItemTouchCallback = object : SimpleCallback( @Composable
UP or DOWN or START or END, RIGHT) { fun CodecsScreen(navigateBack: () -> Unit) {
Scaffold(
override fun onMove( modifier = Modifier
recyclerView: RecyclerView, .fillMaxHeight()
viewHolder: RecyclerView.ViewHolder, .imePadding()
target: RecyclerView.ViewHolder .safeDrawingPadding(),
): Boolean { containerColor = LocalCustomColors.current.background,
val fromPosition = viewHolder.adapterPosition topBar = {
val toPosition = target.adapterPosition TopAppBar(
codecsAdapter.moveItem(fromPosition, toPosition) title = {
codecsAdapter.notifyItemMoved(fromPosition, toPosition) Text(
return true 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)
},
)
} }
@SuppressLint("NotifyDataSetChanged") @Composable
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { fun CodecsContent(contentPadding: PaddingValues) {
val position = viewHolder.adapterPosition Column(
if (newCodecs[position].enabled) { modifier = Modifier
codecsAdapter.disableItem(position) .fillMaxWidth()
codecsAdapter.notifyItemRemoved(position) .background(LocalCustomColors.current.background)
} else { .padding(contentPadding)
codecsAdapter.enableItem(position) .padding(bottom = 16.dp),
codecsAdapter.notifyDataSetChanged() ) {
Title()
Codecs(codecs = codecs, onCodecsChange = { updatedCodecs ->
onCodecsChange(updatedCodecs)
})
} }
} }
@SuppressLint("NotifyDataSetChanged") private fun onCodecsChange(updatedCodecs: List<Codec>) {
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { codecs.clear()
super.clearView(recyclerView, viewHolder) codecs.addAll(updatedCodecs)
recyclerView.post { codecsAdapter.notifyDataSetChanged() } }
@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())
}
)
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
)
}
} }
} }
ItemTouchHelper(simpleItemTouchCallback) private fun updateCodecs() {
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.check_icon, menu)
return true
}
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 var save = false
val codecs = ArrayList<String>() val newCodecs = ArrayList<String>()
for (codec in newCodecs) for (codec in codecs)
if (codec.enabled) if (codec.enabled.value)
codecs.add(codec.name) newCodecs.add(codec.name)
val codecList = Utils.implode(codecs, ",") val codecList = Utils.implode(newCodecs, ",")
if (media == "audio") if (media == "audio")
if (codecs != acc.audioCodec) { if (newCodecs != acc.audioCodec) {
if (Api.account_set_audio_codecs(acc.accp, codecList) == 0) { if (Api.account_set_audio_codecs(acc.accp, codecList) == 0) {
Log.d(TAG, "New audio codecs '$codecList'") Log.d(TAG, "New audio codecs '$codecList'")
acc.audioCodec = codecs acc.audioCodec = newCodecs
save = true save = true
} else { } else {
Log.e(TAG, "Setting of audio codecs '$codecList' failed") Log.e(TAG, "Setting of audio codecs '$codecList' failed")
@@ -186,10 +312,10 @@ class CodecsActivity : AppCompatActivity() {
} }
if (media == "video") if (media == "video")
if (codecs != acc.videoCodec) { if (newCodecs != acc.videoCodec) {
if (Api.account_set_video_codecs(acc.accp, codecList) == 0) { if (Api.account_set_video_codecs(acc.accp, codecList) == 0) {
Log.d(TAG, "New video codecs '$codecs'") Log.d(TAG, "New video codecs '$codecs'")
acc.videoCodec = codecs acc.videoCodec = newCodecs
save = true save = true
} else { } else {
Log.e(TAG, "Setting of video codecs '$codecs' failed") Log.e(TAG, "Setting of video codecs '$codecs' failed")
@@ -201,17 +327,15 @@ class CodecsActivity : AppCompatActivity() {
BaresipService.activities.remove("codecs,$aor,$media") BaresipService.activities.remove("codecs,$aor,$media")
finish() finish()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.check_icon, menu)
return true return true
}
android.R.id.home -> {
goBack()
return true
}
}
return super.onOptionsItemSelected(item)
} }
@@ -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)
}
}
@@ -153,11 +153,10 @@ object Config {
if ("${module}.so" in previousModules) if ("${module}.so" in previousModules)
config = "${config}module ${module}.so\n" config = "${config}module ${module}.so\n"
if ((!Utils.isAecSupported() && !File(configPath).exists()) || Utils.aecAgcCheck()
"webrtc_aecm.so" in previousModules) {
if (!BaresipService.aecAvailable)
config = "${config}module webrtc_aecm.so\n" config = "${config}module webrtc_aecm.so\n"
BaresipService.webrtcAec = true
}
val micGain = previousVariable("augain") val micGain = previousVariable("augain")
config = if (BaresipService.agcAvailable || micGain == "" || micGain == "1.0") config = if (BaresipService.agcAvailable || micGain == "" || micGain == "1.0")
File diff suppressed because it is too large Load Diff
@@ -6,8 +6,6 @@ const val DEFAULT_CHANNEL_ID = "com.tutpro.baresip.default"
const val MEDIUM_CHANNEL_ID = "com.tutpro.baresip.medium" const val MEDIUM_CHANNEL_ID = "com.tutpro.baresip.medium"
const val HIGH_CHANNEL_ID = "com.tutpro.baresip.high" const val HIGH_CHANNEL_ID = "com.tutpro.baresip.high"
const val CONTACTS_PERMISSION_REQUEST_CODE = 1
const val STATUS_NOTIFICATION_ID = 101 const val STATUS_NOTIFICATION_ID = 101
const val CALL_NOTIFICATION_ID = 102 const val CALL_NOTIFICATION_ID = 102
const val CALL_MISSED_NOTIFICATION_ID = 103 const val CALL_MISSED_NOTIFICATION_ID = 103
+102 -62
View File
@@ -7,33 +7,61 @@ import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.provider.ContactsContract import android.provider.ContactsContract
import androidx.core.net.toUri import androidx.core.net.toUri
import com.tutpro.baresip.BaresipService.Companion.contactNames
import java.io.File import java.io.File
import java.util.ArrayList import java.util.ArrayList
sealed class Contact { 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 favorite: Boolean): Contact() {
var avatarImage: Bitmap? = null var avatarImage: Bitmap? = null
} }
class AndroidContact(var name: String, var color: Int, var thumbnailUri: Uri?, 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>() 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 { 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 // Return contact name of uri or uri itself if contact with uri is not found
fun contactName(uri: String): String { fun contactName(uri: String): String {
var contact = findContact(uri) var contact = findContact(uri)
@@ -42,21 +70,29 @@ sealed class Contact {
if (Utils.isTelNumber(userPart)) if (Utils.isTelNumber(userPart))
contact = findContact("tel:$userPart") contact = findContact("tel:$userPart")
} }
if (contact != null) { if (contact != null)
return when (contact) { return contact.name()
is BaresipContact ->
contact.name
is AndroidContact ->
contact.name
}
}
return uri 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 // Return URIs of contact name
fun contactUris(name: String): ArrayList<String> { fun contactUris(name: String): ArrayList<String> {
val uris = ArrayList<String>() val uris = ArrayList<String>()
for (c in contacts()) for (c in BaresipService.contacts)
when (c) { when (c) {
is BaresipContact -> { is BaresipContact -> {
if (c.name.equals(name, ignoreCase = true)) { if (c.name.equals(name, ignoreCase = true)) {
@@ -78,7 +114,7 @@ sealed class Contact {
} }
fun findContact(uri: String): Contact? { fun findContact(uri: String): Contact? {
for (c in contacts()) for (c in BaresipService.contacts)
when (c) { when (c) {
is BaresipContact -> { is BaresipContact -> {
if (Utils.uriMatch(c.uri, uri)) if (Utils.uriMatch(c.uri, uri))
@@ -95,22 +131,16 @@ sealed class Contact {
return null return null
} }
fun nameExists(name: String, ignoreCase: Boolean): Boolean { fun nameExists(name: String, list: List<Contact>, ignoreCase: Boolean): Boolean {
for (c in BaresipService.contacts) for (c in list)
when (c) { if (c.name().equals(name, ignoreCase = ignoreCase))
is BaresipContact ->
if (c.name.equals(name, ignoreCase = ignoreCase))
return true return true
is AndroidContact ->
if (c.name.equals(name, ignoreCase = ignoreCase))
return true
}
return false return false
} }
fun saveBaresipContacts() { fun saveBaresipContacts() {
var contents = "" var contents = ""
for (c in BaresipService.baresipContacts) for (c in BaresipService.baresipContacts.value)
contents += "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}" + contents += "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}" +
";favorite=${if (c.favorite) "yes" else "no"}\n" ";favorite=${if (c.favorite) "yes" else "no"}\n"
Utils.putFileContents(BaresipService.filesPath + "/contacts", Utils.putFileContents(BaresipService.filesPath + "/contacts",
@@ -129,7 +159,7 @@ sealed class Contact {
ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'" ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'"
val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection,
selection, null, null) selection, null, null)
BaresipService.androidContacts.clear() BaresipService.androidContacts.value = listOf()
val contacts = HashMap<Long, AndroidContact>() val contacts = HashMap<Long, AndroidContact>()
while (cur != null && cur.moveToNext()) { while (cur != null && cur.moveToNext()) {
val id = cur.getLong(0) val id = cur.getLong(0)
@@ -141,7 +171,7 @@ sealed class Contact {
val contact = if (contacts.containsKey(id)) val contact = if (contacts.containsKey(id))
contacts[id]!! contacts[id]!!
else else
AndroidContact(name, Utils.randomColor(), thumb, starred == 1) AndroidContact(name, Utils.randomColor(), thumb, id, starred == 1)
if (contact.name == "" && name != "") if (contact.name == "" && name != "")
contact.name = name contact.name = name
if (contact.thumbnailUri == null && thumb != null) if (contact.thumbnailUri == null && thumb != null)
@@ -158,9 +188,11 @@ sealed class Contact {
contacts[id] = contact contacts[id] = contact
} }
cur?.close() cur?.close()
val newList = mutableListOf<AndroidContact>()
for ((_, value) in contacts) for ((_, value) in contacts)
if (value.name != "" && value.uris.isNotEmpty()) if (value.name != "" && value.uris.isNotEmpty())
BaresipService.androidContacts.add(value) newList.add(value)
BaresipService.androidContacts.value = newList.toList()
} }
@Suppress("unused") @Suppress("unused")
@@ -179,7 +211,8 @@ sealed class Contact {
val contacts = String(content) val contacts = String(content)
var contactNo = 0 var contactNo = 0
val baseId = System.currentTimeMillis() val baseId = System.currentTimeMillis()
BaresipService.baresipContacts.clear() val restored = mutableListOf<BaresipContact>()
BaresipService.baresipContacts.value = listOf()
contacts.lines().forEach { contacts.lines().forEach {
val parts = it.split("\"") val parts = it.split("\"")
if (parts.size == 3) { if (parts.size == 3) {
@@ -212,55 +245,62 @@ sealed class Contact {
Log.e(TAG, "Could not read avatar image from '$id.png") 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 return true
} }
fun contactsUpdate() { fun contactsUpdate() {
BaresipService.contacts.clear() val newList = mutableListOf<Contact>()
if (BaresipService.contactsMode != "android") if (BaresipService.contactsMode != "android")
BaresipService.contacts.addAll(BaresipService.baresipContacts) for (c in BaresipService.baresipContacts.value)
newList.add(c.copy())
if (BaresipService.contactsMode != "baresip") if (BaresipService.contactsMode != "baresip")
addAndroidContacts() for (c in BaresipService.androidContacts.value)
sortContacts() 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() generateContactNames()
BaresipService.contactUpdate.postValue(System.nanoTime()) //BaresipService.contactUpdate.postValue(System.nanoTime())
} }
fun addBaresipContact(contact: BaresipContact) { 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) { 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() saveBaresipContacts()
} }
private fun generateContactNames () { private fun generateContactNames () {
BaresipService.contactNames.clear() val newList = mutableListOf<String>()
for (c in BaresipService.contacts) for (c in BaresipService.contacts)
when (c) { when (c) {
is BaresipContact -> is BaresipContact ->
BaresipService.contactNames.add(c.name) newList.add(c.name)
is AndroidContact -> 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
}}
}
}
} }
@@ -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
}
}
}
@@ -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)
}
}
@@ -1,28 +1,71 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity import android.app.Activity
import android.content.* import android.content.ContentValues
import android.os.Build import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.provider.ContactsContract import android.provider.ContactsContract
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem import android.view.MenuItem
import android.view.View import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.graphics.Insets import androidx.compose.foundation.Image
import androidx.core.view.ViewCompat import androidx.compose.foundation.background
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.combinedClickable
import androidx.core.view.WindowInsetsControllerCompat import androidx.compose.foundation.layout.Arrangement
import androidx.core.view.updatePadding 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 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 lateinit var aor: String
private var newAndroidName: String? = null private var newAndroidName: String? = null
private var lastClick: Long = 0 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?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityContactsBinding.inflate(layoutInflater) enableEdgeToEdge()
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat -> val title = String.format(getString(R.string.contacts))
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
aor = intent.getStringExtra("aor")!! aor = intent.getStringExtra("aor")!!
Utils.addActivity("contacts,$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> { val androidContactsObserver = Observer<Long> {
if (newAndroidName != null) { if (newAndroidName != null) {
val contentValues = ContentValues() val contentValues = ContentValues()
@@ -73,64 +110,374 @@ class ContactsActivity : AppCompatActivity() {
} }
newAndroidName = null newAndroidName = null
} }
clAdapter.notifyDataSetChanged() Contact.contactsUpdate()
} }
BaresipService.contactUpdate.observe(this, androidContactsObserver) BaresipService.contactUpdate.observe(this, androidContactsObserver)
val contactRequest = setContent {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { AppTheme {
if (it.resultCode == RESULT_OK) { Surface(
if (it.data != null && it.data!!.hasExtra("name")) modifier = Modifier.fillMaxSize(),
newAndroidName = it.data!!.getStringExtra("name") color = LocalCustomColors.current.background
) {
ContactsScreen(LocalContext.current, title) {
goBack()
} }
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)
} }
} }
} }
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
} }
override fun onResume() { @OptIn(ExperimentalMaterial3Api::class)
super.onResume() @Composable
clAdapter.notifyDataSetChanged() 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> android.R.id.home ->
goBack() goBack()
} }
return true return true
} }
private fun goBack() { private fun goBack() {
BaresipService.activities.remove("contacts,$aor") BaresipService.activities.remove("contacts,$aor")
setResult(Activity.RESULT_OK, Intent()) setResult(RESULT_OK, Intent())
finish() finish()
} }
} }
@@ -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() }
@@ -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.",
)
}
}
@@ -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
@@ -8,32 +8,76 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
var new: Boolean): Serializable { var new: Boolean): Serializable {
fun add() { fun add() {
BaresipService.messages.add(this) val updatedMessages = BaresipService.messages.toMutableList()
updatedMessages.add(this)
BaresipService.messages = updatedMessages.toList()
var count = 0 var count = 0
var firstIndex = -1 var remove: Message? = null
for (i in BaresipService.messages.indices) for (message in BaresipService.messages)
if (BaresipService.messages[i].aor == this.aor) { if (message.aor == this.aor) {
if (count == 0) firstIndex = i
count++ count++
if (count > MESSAGE_HISTORY_SIZE) { if (count > MESSAGE_HISTORY_SIZE) {
remove = message
break break
} }
} }
if (count > MESSAGE_HISTORY_SIZE) if (remove != null)
BaresipService.messages.removeAt(firstIndex) 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 { companion object {
const val MESSAGE_HISTORY_SIZE = 100 const val MESSAGE_HISTORY_SIZE = 100
fun messages(): ArrayList<Message> { fun messages(): List<Message> {
return BaresipService.messages return BaresipService.messages
} }
fun clear(aor: String) { fun clearMessagesOfAor(aor: String) {
val it = BaresipService.messages.iterator() val updatedMessages = BaresipService.messages.toMutableList()
val it = updatedMessages.iterator()
while (it.hasNext()) if (it.next().aor == aor) it.remove() 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() { fun save() {
@@ -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
}
}
@@ -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() {}
}
@@ -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
}
}
@@ -1,5 +1,8 @@
package com.tutpro.baresip package com.tutpro.baresip
import com.tutpro.baresip.BaresipService.Companion.uas
import com.tutpro.baresip.BaresipService.Companion.uasStatus
class UserAgent(val uap: Long) { class UserAgent(val uap: Long) {
val account = Account(Api.ua_account(uap)) val account = Account(Api.ua_account(uap))
@@ -10,11 +13,22 @@ class UserAgent(val uap: Long) {
} }
fun add() { fun add() {
BaresipService.uas.add(this) val updatedUas = uas.value.toMutableList()
updatedUas.add(this)
uas.value = updatedUas.toList()
uasStatus.value = statusMap()
} }
fun remove() { 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> { fun calls(dir: String = ""): ArrayList<Call> {
@@ -34,19 +48,19 @@ class UserAgent(val uap: Long) {
companion object { companion object {
fun ofAor(aor: String): UserAgent? { fun ofAor(aor: String): UserAgent? {
for (ua in BaresipService.uas) for (ua in uas.value)
if (ua.account.aor == aor) return ua if (ua.account.aor == aor) return ua
return null return null
} }
fun ofDomain(domain: String): UserAgent? { fun ofDomain(domain: String): UserAgent? {
for (ua in BaresipService.uas) for (ua in uas.value)
if (Utils.aorDomain(ua.account.aor) == domain) return ua if (Utils.aorDomain(ua.account.aor) == domain) return ua
return null return null
} }
fun ofUap(uap: Long): UserAgent? { fun ofUap(uap: Long): UserAgent? {
for (ua in BaresipService.uas) for (ua in uas.value)
if (ua.uap == uap) return ua if (ua.uap == uap) return ua
return null return null
} }
@@ -59,20 +73,27 @@ class UserAgent(val uap: Long) {
} }
fun findAorIndex(aor: String): Int? { fun findAorIndex(aor: String): Int? {
for (i in BaresipService.uas.indices) { for (i in uas.value.indices) {
if (BaresipService.uas[i].account.aor == aor) return i if (uas.value[i].account.aor == aor) return i
} }
return null return null
} }
fun register() { fun register() {
for (ua in BaresipService.uas) { for (ua in uas.value) {
if (ua.account.regint > 0) { if (ua.account.regint > 0) {
if (Api.ua_register(ua.uap) != 0) if (Api.ua_register(ua.uap) != 0)
Log.d(TAG, "Failed to register ${ua.account.aor}") 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
}
} }
} }
+79 -82
View File
@@ -7,44 +7,60 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Bitmap.createScaledBitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color
import android.media.AudioAttributes 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.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer import android.media.MediaPlayer
import android.media.audiofx.AcousticEchoCanceler 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.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi 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.content.ContextCompat
import androidx.core.graphics.createBitmap
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import java.io.BufferedInputStream
import java.io.* 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.lang.reflect.Method
import java.net.InetAddress import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
import java.security.SecureRandom import java.security.SecureRandom
import java.text.DateFormat 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.concurrent.Executor
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
@@ -54,6 +70,7 @@ import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import androidx.core.graphics.scale
object Utils { object Utils {
@@ -161,7 +178,7 @@ object Utils {
return u return u
} }
fun e164Uri(uri: String, countryCode: String): String { private fun e164Uri(uri: String, countryCode: String): String {
if (countryCode == "") return uri if (countryCode == "") return uri
val scheme = uri.substring(0, 4) val scheme = uri.substring(0, 4)
val userPart = uriUserPart(uri) val userPart = uriUserPart(uri)
@@ -488,30 +505,6 @@ object Utils {
return result 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 { fun checkPermissions(ctx: Context, permissions: Array<String>) : Boolean {
for (p in permissions) { for (p in permissions) {
if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) {
@@ -524,23 +517,6 @@ object Utils {
return true 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) { fun copyAssetToFile(context: Context, asset: String, path: String) {
try { try {
val `is` = context.assets.open(asset) val `is` = context.assets.open(asset)
@@ -649,7 +625,7 @@ object Utils {
if (file.exists()) file.delete() if (file.exists()) file.delete()
try { try {
val out = FileOutputStream(file) val out = FileOutputStream(file)
val scaledBitmap = createScaledBitmap(bitmap, 96, 96, true) val scaledBitmap = bitmap.scale(96, 96)
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out) scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush() out.flush()
out.close() out.close()
@@ -663,7 +639,6 @@ object Utils {
class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable { class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable {
companion object { companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID: Long = -29238082928391L private const val serialVersionUID: Long = -29238082928391L
} }
} }
@@ -862,7 +837,7 @@ object Utils {
fun randomColor(): Int { fun randomColor(): Int {
val rnd = Random() 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)) 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 { fun relativeTime(ctx: Context, time: GregorianCalendar): String {
return if (DateUtils.isToday(time.timeInMillis)) { return if (DateUtils.isToday(time.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.SHORT) val fmt = DateFormat.getTimeInstance(DateFormat.SHORT)
@@ -894,8 +882,8 @@ object Utils {
} }
} }
fun bitmapFromView(view: View): Bitmap { private fun bitmapFromView(view: View): Bitmap {
val bitmap = Bitmap.createBitmap(view.layoutParams.width, view.layoutParams.height, Bitmap.Config.ARGB_8888) val bitmap = createBitmap(view.layoutParams.width, view.layoutParams.height)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
view.layout(0, 0, view.layoutParams.width, view.layoutParams.height) view.layout(0, 0, view.layoutParams.width, view.layoutParams.height)
view.draw(canvas) view.draw(canvas)
@@ -1044,32 +1032,41 @@ object Utils {
} }
} }
fun isDarkTheme(ctx: Context): Boolean { fun aecAgcCheck() {
return ctx.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
fun isAecSupported(): Boolean { val sessionId = Api.AAudio_open_stream()
if (!AcousticEchoCanceler.isAvailable()) {
Log.i(TAG, "Hardware AEC is NOT available")
return false
}
val sessionId = Api.create_AAudio_SessionId()
if (sessionId == -1) { if (sessionId == -1) {
Log.e(TAG, "Failed to create AAudio stream for session ID") Log.e(TAG, "Failed to open AAudio stream")
return false return
} }
if (AcousticEchoCanceler.isAvailable()) {
val aec = AcousticEchoCanceler.create(sessionId) val aec = AcousticEchoCanceler.create(sessionId)
return if (aec != null) { if (aec != null) {
BaresipService.aecAvailable = true
aec.release() aec.release()
Log.i(TAG, "Hardware AEC is supported") Log.d(TAG, "Creation of hardware AEC for $sessionId succeeded")
true
} else { } else {
Log.w(TAG, "Hardware AEC is NOT supported") Log.w(TAG, "Creation of hardware AEC for $sessionId failed")
false
} }
} }
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()
} }
}
@@ -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
}
}
@@ -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>
@@ -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>
@@ -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>
@@ -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>
+1 -1
View File
@@ -4,6 +4,6 @@
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" > android:viewportHeight="960" >
<path <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"/> 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> </vector>
+1 -1
View File
@@ -4,6 +4,6 @@
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" > android:viewportHeight="960" >
<path <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"/> 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> </vector>
+1 -2
View File
@@ -1,6 +1,5 @@
<vector android:height="48dp" android:tint="@color/colorAccent" <vector android:height="48dp" android:tint="@color/colorAccent"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" <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"/>
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> </vector>
+5
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>
@@ -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>
-34
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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
-264
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>
@@ -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>
@@ -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>
-60
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>
@@ -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>
@@ -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>
-384
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>
@@ -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>
@@ -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>
-403
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>
-9
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>
@@ -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>
@@ -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>
-96
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>
@@ -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>
-93
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>
-36
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>
-66
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>
-39
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>
+5 -2
View File
@@ -131,7 +131,11 @@
<string name="start_automatically">Стартирайте автоматично</string> <string name="start_automatically">Стартирайте автоматично</string>
<string name="start_automatically_help">Ако е отметнато, baresip се стартира автоматично след включване на устройството.</string> <string name="start_automatically_help">Ако е отметнато, baresip се стартира автоматично след включване на устройството.</string>
<string name="listen_address">Слушане на адрес</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="invalid_listen_address">Невалиден адрес за слушане</string>
<string name="dns_servers">DNS сървъри</string> <string name="dns_servers">DNS сървъри</string>
<string name="dns_servers_help">Списък разделен със запетая на адреси на DNS сървъри. Ако не е зададено, <string name="dns_servers_help">Списък разделен със запетая на адреси на DNS сървъри. Ако не е зададено,
@@ -147,7 +151,6 @@
<string name="tls_ca_file">TLS CA File</string> <string name="tls_ca_file">TLS CA File</string>
<string name="tls_ca_file_help">Ако е поставено отметка, файл \'ca_certs.crt\' съдържащи TLS сертификати на Сертифициращите органи е бил или ще бъде зареден от директорията за Изтегляне.</string> <string name="tls_ca_file_help">Ако е поставено отметка, файл \'ca_certs.crt\' съдържащи TLS сертификати на Сертифициращите органи е бил или ще бъде зареден от директорията за Изтегляне.</string>
<string name="aec">Акустично отменяне на ехото</string> <string name="aec">Акустично отменяне на ехото</string>
<string name="aec_help">Ако е поставена отметка, се прави опит за отмяна на ехо при аудио повикване.</string>
<string name="opus_bit_rate">Opus Bit Rate</string> <string name="opus_bit_rate">Opus Bit Rate</string>
<string name="opus_bit_rate_help">Средна максимална битова скорост, използвана от аудио поток на Opus. <string name="opus_bit_rate_help">Средна максимална битова скорост, използвана от аудио поток на Opus.
Валидните стойности са 6000-510000. Фабрично по подразбиране е 28000. Валидните стойности са 6000-510000. Фабрично по подразбиране е 28000.
-3
View File
@@ -237,7 +237,6 @@
<string name="calls_call">volání</string> <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="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="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_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">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> <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="default_call_volume">Výchozí hlasitost volání</string>
<string name="dark_theme_help">Vynutit tmavé téma</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="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="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_packet_loss">Neplatné procento ztráty paketů Opus</string>
<string name="invalid_opus_bitrate">Neplatný datový tok 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> <string name="default_call_volume_help">Pokud je nastaveno, je výchozí hlasitost zvuku hovoru na stupnici 1-10.</string>
-3
View File
@@ -359,7 +359,6 @@
<string name="video_codecs">Video Codecs</string> <string name="video_codecs">Video Codecs</string>
<string name="audio_settings">Audioeinstellungen</string> <string name="audio_settings">Audioeinstellungen</string>
<string name="backup">Sicherungskopie</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="rtcp_mux">RTCP Multiplexverfahren</string>
<string name="invalid_opus_packet_loss">Ungültiger Opus Paketverlustprozentsatz</string> <string name="invalid_opus_packet_loss">Ungültiger Opus Paketverlustprozentsatz</string>
<string name="default_call_volume">Standardmäßige Anruflautstärke</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="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_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="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">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_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> <string name="opus_packet_loss">Erwarteter Paketverlust von Opus</string>
+6 -4
View File
@@ -139,7 +139,12 @@
<string name="start_automatically">Comenzar automáticamente</string> <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="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">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="invalid_listen_address">Dirección de escucha no válida</string>
<string name="dns_servers">Servidores DNS</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, <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">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="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">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">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. <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> 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="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_help">Forzar el tema de la pantalla oscura</string>
<string name="dark_theme">Tema oscuro</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_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="verify_server">Verificar los certificados del servidor</string>
<string name="transfer_request_to">Solicitud de transferencia de llamada a</string> <string name="transfer_request_to">Solicitud de transferencia de llamada a</string>
+16 -12
View File
@@ -6,8 +6,10 @@
<string name="about_text"> <string name="about_text">
<![CDATA[ <![CDATA[
<h1>Baresip-kirjastoon perustuva, äänipuhelut ja pikaviestit mahdollistava SIP-sovellus</h1> <h1>Baresip-kirjastoon perustuva, äänipuhelut ja pikaviestit mahdollistava SIP-sovellus</h1>
<br>
<p>Juha Heinanen &lt;[email protected]&gt;</p> <p>Juha Heinanen &lt;[email protected]&gt;</p>
<p>Versio %1$s</p> <p>Versio %1$s</p>
<br>
<h2>Käyttövihjeitä</h2> <h2>Käyttövihjeitä</h2>
<ul> <ul>
<li>Tarkista, että baresip-sovelluksen Asetukset vastaavat tarpeitasi. Kunkin otsikon kosketus tarjoaa apua.</li> <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> 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>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>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 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>Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.</li>
<li>Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.</li> <li>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> <li>Katso lisätietoja <a href=https://github.com/juha-h/baresip-studio/wiki>Wiki</a>-sivulta.</li>
</ul> </ul>
<h2>Tietosuoja</h2> <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> <h2>Lähdekoodi</h2>
Lähdekoodi on saatavilla <a href=https://github.com/juha-h/baresip-studio>GitHub</a>:ssa, <p>Lähdekoodi on saatavilla <a href=https://github.com/juha-h/baresip-studio>GitHub</a>:ssa,
missä voi myös raportoida virheistä. missä voi myös raportoida virheistä.</p>
<br>
<h2>Lisenssit</h2> <h2>Lisenssit</h2>
<ul> <ul>
<li><b>BSD-3-Clause</b> except the following:</li> <li><b>BSD-3-Clause</b> except the following:</li>
@@ -223,9 +226,9 @@
</string> </string>
<!-- Accounts Activity --> <!-- Accounts Activity -->
<string name="accounts">Tilit</string> <string name="accounts">Tilit</string>
<string name="new_account">Uusi tili</string> <string name="new_account">Uuden tilin SIP URI</string>
<string name="accounts_help">Kun uusi tili luodaan, voidaan haluttaessa antaa myös porttinumero <string name="accounts_help">Uuden tilin SIP URI muotoa
ja tiedonsiirtoprotokolla: &lt;käyttäjä&gt;@&lt;domain&gt;[:&lt;portti&gt;][;transport=udp|tcp|tls]. &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; 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 &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 Jos kumpaakaan ei ole annettu eikä välityspalvelinta ole määritelty, tilin mahdollinen
@@ -293,11 +296,11 @@
<!-- Codecs Activity --> <!-- Codecs Activity -->
<string name="audio_codecs">Audion koodausmenetelmät</string> <string name="audio_codecs">Audion koodausmenetelmät</string>
<string name="audio_codecs_help">Luettelo audion koodausmenetelmistä prioriteettijärjestyksessä. <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>
<string name="video_codecs">Videon koodausmenetelmät</string> <string name="video_codecs">Videon koodausmenetelmät</string>
<string name="video_codecs_help">Luettelo videon koodausmenetelmistä prioriteettijärjestyksessä. <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>
<string name="codec_action">Järjestä</string> <string name="codec_action">Järjestä</string>
<!-- Config Activity --> <!-- Config Activity -->
@@ -365,9 +368,6 @@
</string> </string>
<string name="failed_to_load_module">Modulin lataaminen epäonnistui.</string> <string name="failed_to_load_module">Modulin lataaminen epäonnistui.</string>
<string name="aec">Akustinen kaiun poisto (AKP)</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">Mikrofonin äänikerroin</string>
<string name="microphone_gain_help">Kertoo mikrofonin äänen voimakkuuden tällä desimaaliluvulla. <string name="microphone_gain_help">Kertoo mikrofonin äänen voimakkuuden tällä desimaaliluvulla.
Minimikerroin on 1.0 (tehdasasetus), mikä ei muuta mikrofonin äänen voimakkuutta. Suuremmat 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="sip_or_tel_uri">SIP tai tel URI</string>
<string name="user_domain_or_number">käyttäjä@domain tai puhelinnumero</string> <string name="user_domain_or_number">käyttäjä@domain tai puhelinnumero</string>
<string name="favorite">Suosikki</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="invalid_contact">Virheellinen yhteystiedon nimi \'%1$s\'</string>
<string name="contact_already_exists">Yhteystieto \'%1$s\' on jo olemassa.</string> <string name="contact_already_exists">Yhteystieto \'%1$s\' on jo olemassa.</string>
<string name="invalid_contact_uri">Virheellinen SIP:n URI</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="invalid_sip_or_tel_uri">Virheellinen SIP tai puh. URI \'%1$s\'</string>
<string name="baresip" translatable="false">baresip</string> <string name="baresip" translatable="false">baresip</string>
<string name="android" translatable="false">Android</string> <string name="android" translatable="false">Android</string>
<string name="hardware">Laitteisto</string>
<string name="software">Ohjelmisto</string>
<!-- Main Activity --> <!-- Main Activity -->
<string name="backup">Tallenna</string> <string name="backup">Tallenna</string>
<string name="restore">Palauta</string> <string name="restore">Palauta</string>
+6 -4
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 name="calls_add_delete_question">Do you want to add \'%1$s\' to contacts or delete %2$s from call history?
</string> </string>
<string name="long_chat_question">Do you want to delete chat with peer \'%1$s\' or add peer to contacts?</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, <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 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 \'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="tls_ca_file">TLS CA証明書ファイル</string>
<string name="audio_modules_title">オーディオモジュール</string> <string name="audio_modules_title">オーディオモジュール</string>
<string name="failed_to_load_module">モジュールの読み込みに失敗しました</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_bit_rate">Opusのビットレート</string>
<string name="opus_packet_loss">期待されるOpusのパケットロス</string> <string name="opus_packet_loss">期待されるOpusのパケットロス</string>
<string name="invalid_opus_bitrate">Opusのビットレートが無効です</string> <string name="invalid_opus_bitrate">Opusのビットレートが無効です</string>
@@ -346,7 +349,6 @@
<string name="answer_mode_help">着信した電話にどのように応答するかを選択します。</string> <string name="answer_mode_help">着信した電話にどのように応答するかを選択します。</string>
<string name="start_automatically_help">チェックを入れると、デバイスの再起動後に自動的にbaresipが起動します</string> <string name="start_automatically_help">チェックを入れると、デバイスの再起動後に自動的にbaresipが起動します</string>
<string name="aec">音響エコーキャンセル</string> <string name="aec">音響エコーキャンセル</string>
<string name="aec_extended_filter_help">チェックを入れると、エコーキャンセルは拡張フィルタを使用しています。</string>
<string name="default_call_volume_help">設定されている場合、デフォルトの通話音声の音量は1~10段階です</string> <string name="default_call_volume_help">設定されている場合、デフォルトの通話音声の音量は1~10段階です</string>
<string name="dark_theme_help">ダークテーマを強制する</string> <string name="dark_theme_help">ダークテーマを強制する</string>
<string name="debug_help">チェックを入れると、デバッグおよび情報レベルのログメッセージをLogcatに提供します。</string> <string name="debug_help">チェックを入れると、デバッグおよび情報レベルのログメッセージをLogcatに提供します。</string>
@@ -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="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="failed_to_load_module">Klarte ikke å laste inn modul.</string>
<string name="aec">Kansellering av akustisk ekko</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_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">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> <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>
@@ -91,7 +91,6 @@
<string name="opus_packet_loss">Perda de pacotes Opus esperada</string> <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_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="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="aec">Cancelamento de Eco Acústico</string>
<string name="failed_to_load_module">Falha ao carregar módulo.</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> <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="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_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="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="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_help">Senha, caso seja necessário pelo servidor STUN/TURN</string>
<string name="stun_password">Senha 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="invalid_microphone_gain">Valor inválido do ganho do microfone</string>
<string name="microphone_gain">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="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> </resources>
-3
View File
@@ -43,7 +43,6 @@
<string name="chat">Mensagem do Chat</string> <string name="chat">Mensagem do Chat</string>
<string name="contact_action_question">Quer ligar ou enviar mensagem para \'%1$s\'\?</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="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="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="contact_already_exists">Contato \'%1$s\' já existe.</string>
<string name="sip_uri_of_proxy_server">URI SIP do Servidor Proxy</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="no_messages">Não tem mensagens</string>
<string name="duration">Duração: %1$d (segs)</string> <string name="duration">Duração: %1$d (segs)</string>
<string name="transferring_call_to_dots">Transferindo chamada para …</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="call_info">Informações da Chamada</string>
<string name="chats">Histórico de Mensagens</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="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="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="restart_request">Solicitação para Reinicialização</string>
<string name="register">Registo</string> <string name="register">Registo</string>
<string name="aec_extended_filter">Filtro Estendido do AEC</string>
<string name="restore">Restaurar</string> <string name="restore">Restaurar</string>
<string name="opus_packet_loss">Perda de pacotes Opus esperada</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> <string name="transfer_request_query">Aceita transferir esta chamada para \'%1$s\'\?</string>
-1
View File
@@ -62,7 +62,6 @@
<string name="listen_address">Adresă de conectare</string> <string name="listen_address">Adresă de conectare</string>
<string name="dns_servers">Servere DNS</string> <string name="dns_servers">Servere DNS</string>
<string name="aec">Anularea acustică a ecoului</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="opus_bit_rate">Rată de eșantionare Opus</string>
<string name="default_call_volume">Volum implicit de apel</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> <string name="default_call_volume_help">Dacă este setat, volumul audio de apel implicit între 110.</string>
-3
View File
@@ -134,7 +134,6 @@
<string name="account_exists">Профиль \'%1$s\' уже существует.</string> <string name="account_exists">Профиль \'%1$s\' уже существует.</string>
<string name="accounts">Профили</string> <string name="accounts">Профили</string>
<string name="aec">Подавление акустического эха</string> <string name="aec">Подавление акустического эха</string>
<string name="aec_help">Если выбрано - работает подавление эха.</string>
<string name="and">и</string> <string name="and">и</string>
<string name="answer_mode">Режим ответа</string> <string name="answer_mode">Режим ответа</string>
<string name="answer_mode_help">Выберите как отвечать на входящие вызовы.</string> <string name="answer_mode_help">Выберите как отвечать на входящие вызовы.</string>
@@ -181,8 +180,6 @@
<string name="opus_packet_loss">Ожидаемая потеря пакетов Opus</string> <string name="opus_packet_loss">Ожидаемая потеря пакетов Opus</string>
<string name="opus_bit_rate_help">Средняя максимальная скорость передачи данных, используемая в аудиопотоке Opus. Допустимые значения: 6000-510000. Значение по умолчанию - 28000.</string> <string name="opus_bit_rate_help">Средняя максимальная скорость передачи данных, используемая в аудиопотоке Opus. Допустимые значения: 6000-510000. Значение по умолчанию - 28000.</string>
<string name="opus_bit_rate">Скорость передачи данных Opus</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="audio_modules_help">Аудиокодеки, предоставленные выбранными модулями, доступны для использования в аккаунтах.</string>
<string name="tls_ca_file_help">При отметке файл с TLS-сертификатами необходимых центров сертификации был или будет загружен. В Android версии 9 и ниже файл называется \'ca_certs.crt\' и берётся из каталога загрузок.</string> <string name="tls_ca_file_help">При отметке файл с TLS-сертификатами необходимых центров сертификации был или будет загружен. В Android версии 9 и ниже файл называется \'ca_certs.crt\' и берётся из каталога загрузок.</string>
<string name="tls_ca_file">Файл TLS CA</string> <string name="tls_ca_file">Файл TLS CA</string>
-3
View File
@@ -219,12 +219,9 @@
<string name="transfer_request">Kopplingsbegäran</string> <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="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="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="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="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="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="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="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> <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>
-2
View File
@@ -171,8 +171,6 @@
<string name="audio_settings">ஆடியோ அமைப்புகள்</string> <string name="audio_settings">ஆடியோ அமைப்புகள்</string>
<string name="tls_ca_file_help">சரிபார்க்கப்பட்டால், ஆண்ட்ராய்டு OS இல் சேர்க்கப்படாத அத்தகைய சான்றிதழ் அதிகாரிகளின் TLS சான்றிதழ்களைக் கொண்ட ஒரு கோப்பு அல்லது ஏற்றப்படும். ஆண்ட்ராய்டு பதிப்பு 9 இல், \'Ca_certs.crt\' என்ற கோப்பு பதிவிறக்க கோப்புறையிலிருந்து ஏற்றப்படுகிறது.</string> <string name="tls_ca_file_help">சரிபார்க்கப்பட்டால், ஆண்ட்ராய்டு OS இல் சேர்க்கப்படாத அத்தகைய சான்றிதழ் அதிகாரிகளின் TLS சான்றிதழ்களைக் கொண்ட ஒரு கோப்பு அல்லது ஏற்றப்படும். ஆண்ட்ராய்டு பதிப்பு 9 இல், \'Ca_certs.crt\' என்ற கோப்பு பதிவிறக்க கோப்புறையிலிருந்து ஏற்றப்படுகிறது.</string>
<string name="speaker_phone">அவைத்தலைவர் தொலைபேசி</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="microphone_gain_help">இந்த தசம எண்ணால் மைக்ரோஃபோன் அளவைப் பெருக்கவும். மைக்ரோஃபோன் ஆதாயத்தை முடக்கும் குறைந்தபட்ச மதிப்பு 1.0 (தொழிற்சாலை இயல்புநிலை) ஆகும். பெரிய மதிப்புகள் ஆடியோ தரத்தை எதிர்மறையாக பாதிக்கலாம்.</string>
<string name="speaker_phone_help">சரிபார்க்கப்பட்டால், அழைப்பு தொடங்கும் போது ச்பீக்கர் தொலைபேசி தானாக இயக்கப்படும்.</string> <string name="speaker_phone_help">சரிபார்க்கப்பட்டால், அழைப்பு தொடங்கும் போது ச்பீக்கர் தொலைபேசி தானாக இயக்கப்படும்.</string>
<string name="audio_modules_title">ஆடியோ தொகுதிகள்</string> <string name="audio_modules_title">ஆடியோ தொகுதிகள்</string>
+18 -16
View File
@@ -8,8 +8,10 @@
<string name="about_text"> <string name="about_text">
<![CDATA[ <![CDATA[
<h1>Baresip library based SIP User Agent</h1> <h1>Baresip library based SIP User Agent</h1>
<br>
<p>Juha Heinanen &lt;[email protected]&gt;</p> <p>Juha Heinanen &lt;[email protected]&gt;</p>
<p>Version %1$s</p> <p>Version %1$s</p>
<br>
<h2>Usage Hints</h2> <h2>Usage Hints</h2>
<ul> <ul>
<li>Check that default values in baresip\'s Settings meet your needs <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>Touch on the dot leads directly to account configuration.</li>
<li>Swipe down gesture causes re-registration of the currently shown account.</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>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>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>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>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>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 <li>See <a href="https://github.com/juha-h/baresip-studio/wiki">Wiki</a> for more information.</li>
information.</li>
</ul> </ul>
<h2>Privacy Policy</h2> <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> <h2>Source code</h2>
Source code is available at <a href="https://github.com/juha-h/baresip-studio">GitHub</a>, <p>Source code is available at <a href="https://github.com/juha-h/baresip-studio">GitHub</a>,
where also issues can be reported. where also issues can be reported.</p>
<br>
<h2>Licenses</h2> <h2>Licenses</h2>
<ul> <ul>
<li><b>BSD-3-Clause</b> except the following:</li> <li><b>BSD-3-Clause</b> except the following:</li>
@@ -210,10 +212,10 @@
</string> </string>
<!-- Accounts Activity --> <!-- Accounts Activity -->
<string name="accounts">Accounts</string> <string name="accounts">Accounts</string>
<string name="new_account">New Account</string> <string name="new_account">SIP URI of New Account</string>
<string name="accounts_help">When a new account is created, account\'s port number and transport <string name="accounts_help">SIP URI of new account of form
protocol may be optionally given: &lt;user&gt;@&lt;domain&gt;[:&lt;port&gt;][;transport=udp|tcp|tls]. &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 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 &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 If neither is given and no outbound proxy is specified, account\'s registrar (if any) is
determined solely based on domain\'s DNS information. determined solely based on domain\'s DNS information.
@@ -278,10 +280,10 @@
<!-- Codecs Activity --> <!-- Codecs Activity -->
<string name="audio_codecs">Audio Codecs</string> <string name="audio_codecs">Audio Codecs</string>
<string name="audio_codecs_help">List of audio codecs in priority order. Drag to reorder, <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">Video Codecs</string>
<string name="video_codecs_help">List of video codecs in priority order. Drag to reorder, <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> <string name="codec_action">Reorder</string>
<!-- Config Activity --> <!-- Config Activity -->
<string name="configuration">Settings</string> <string name="configuration">Settings</string>
@@ -340,10 +342,6 @@
available for use by the accounts.</string> available for use by the accounts.</string>
<string name="failed_to_load_module">Failed to load module.</string> <string name="failed_to_load_module">Failed to load module.</string>
<string name="aec">Acoustic Echo Cancellation</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">Microphone Gain</string>
<string name="microphone_gain_help">Multiply microphone volume by this decimal number. Minimum <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 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="sip_or_tel_uri">SIP or tel URI</string>
<string name="user_domain_or_number">user@domain or telephone number</string> <string name="user_domain_or_number">user@domain or telephone number</string>
<string name="favorite">Favorite</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="invalid_contact">Invalid contact name \'%1$s\'</string>
<string name="contact_already_exists">Contact \'%1$s\' already exists.</string> <string name="contact_already_exists">Contact \'%1$s\' already exists.</string>
<string name="invalid_contact_uri">Invalid SIP URI</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="invalid_sip_or_tel_uri">Invalid SIP or tel URI \'%1$s\'</string>
<string name="baresip" translatable="false">baresip</string> <string name="baresip" translatable="false">baresip</string>
<string name="android" translatable="false">Android</string> <string name="android" translatable="false">Android</string>
<string name="hardware">Hardware</string>
<string name="software">Software</string>
<!-- Main Activity --> <!-- Main Activity -->
<string name="backup">Backup</string> <string name="backup">Backup</string>
<string name="restore">Restore</string> <string name="restore">Restore</string>
+12 -49
View File
@@ -1,50 +1,23 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> <style name="AlertDialogTheme" parent="@style/ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="colorPrimary">@color/colorPrimary</item> <item name="materialAlertDialogTitleTextStyle">@style/Alert.Title</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="materialAlertDialogBodyTextStyle">@style/Alert.Message</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">
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item> <item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
<item name="buttonBarNegativeButtonStyle">@style/Alert.Button.Positive</item> <item name="buttonBarNegativeButtonStyle">@style/Alert.Button.Positive</item>
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item> <item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
<item name="materialAlertDialogBodyTextStyle">@style/Alert.Message</item> <item name="android:background">@color/colorGrayLight</item>
<item name="materialAlertDialogTitleTextStyle">@style/Alert.Title</item>
<item name="shapeAppearanceOverlay">@style/DialogCorners</item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
</style> </style>
<style name="DialogCorners"> <style name="Alert.Title" parent="@style/MaterialAlertDialog.Material3.Title.Text">
<item name="cornerFamily">rounded</item> <item name="android:textColor">@color/colorAlert</item>
<item name="cornerSize">16dp</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>
<style name="Alert.Button.Positive" parent="Widget.Material3.Button.TextButton"> <style name="Alert.Button.Positive" parent="Widget.Material3.Button.TextButton">
@@ -61,14 +34,4 @@
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
</style> </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> </resources>
+1 -1
View File
@@ -8,7 +8,7 @@ buildscript {
} }
dependencies { dependencies {
// noinspection AndroidGradlePluginVersion // noinspection AndroidGradlePluginVersion
classpath 'com.android.tools.build:gradle:8.8.1' classpath libs.gradle.v881
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
+68
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" }
+1 -1
View File
@@ -1,6 +1,6 @@
#Mon Jan 30 05:45:34 EET 2023 #Mon Jan 30 05:45:34 EET 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists